first commit
This commit is contained in:
commit
e382fab275
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
venv
|
||||||
|
config.py
|
BIN
__pycache__/config.cpython-313.pyc
Normal file
BIN
__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
173
app.py
Normal file
173
app.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
from flask import Flask, request, jsonify, render_template
|
||||||
|
from influxdb import InfluxDBClient
|
||||||
|
from flask_cors import CORS
|
||||||
|
from config import get_influx_client
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
client = get_influx_client()
|
||||||
|
|
||||||
|
def escape_identifier(identifier):
|
||||||
|
"""Escapuje identyfikatory dla InfluxDB."""
|
||||||
|
return f'"{identifier.replace("\\", "\\\\").replace("\"", "\\\"")}"'
|
||||||
|
|
||||||
|
def escape_value(value):
|
||||||
|
"""Escapuje wartości dla zapytań InfluxDB."""
|
||||||
|
if isinstance(value, str):
|
||||||
|
escaped = value.replace("'", "''")
|
||||||
|
return f"'{escaped}'"
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
@app.route('/databases', methods=['GET'])
|
||||||
|
def get_databases():
|
||||||
|
databases = client.get_list_database()
|
||||||
|
return jsonify([db['name'] for db in databases])
|
||||||
|
|
||||||
|
@app.route('/series', methods=['GET'])
|
||||||
|
def get_series():
|
||||||
|
db = request.args.get('db')
|
||||||
|
if not db:
|
||||||
|
return jsonify({'error': 'Baza danych nie została wybrana'}), 400
|
||||||
|
|
||||||
|
client.switch_database(db)
|
||||||
|
series_info = []
|
||||||
|
|
||||||
|
#print("DEBUG: Pobieram listę measurementów...")
|
||||||
|
measurements = client.query("SHOW MEASUREMENTS")
|
||||||
|
measurement_names = [m['name'] for m in measurements.get_points()]
|
||||||
|
#print("DEBUG: Measurementy:", measurement_names)
|
||||||
|
|
||||||
|
for measurement in measurement_names:
|
||||||
|
if measurement == "results":
|
||||||
|
print("DEBUG: Pomijam 'results'")
|
||||||
|
continue
|
||||||
|
|
||||||
|
#print(f"DEBUG: SHOW SERIES FROM \"{measurement}\"")
|
||||||
|
try:
|
||||||
|
result = client.query(f'SHOW SERIES FROM "{measurement}"')
|
||||||
|
for s in result.get_points():
|
||||||
|
tags = {k: v for k, v in s.items() if k != 'time'}
|
||||||
|
if not tags:
|
||||||
|
continue
|
||||||
|
series_id = f"{measurement},{','.join([f'{k}={v}' for k, v in tags.items()])}"
|
||||||
|
#print(f"DEBUG: Series = {series_id}")
|
||||||
|
series_info.append({
|
||||||
|
'series': series_id,
|
||||||
|
'measurement': measurement,
|
||||||
|
'tags': tags
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: Błąd w {measurement}: {str(e)}")
|
||||||
|
|
||||||
|
print(f"DEBUG: Zwracam do frontu {len(series_info)} serii")
|
||||||
|
return jsonify(series_info)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/delete', methods=['POST'])
|
||||||
|
def delete_series():
|
||||||
|
data = request.json
|
||||||
|
db = data.get('db')
|
||||||
|
if not db:
|
||||||
|
return jsonify({'error': 'Baza danych nie została wybrana'}), 400
|
||||||
|
|
||||||
|
client.switch_database(db)
|
||||||
|
deleted_count = 0
|
||||||
|
|
||||||
|
for series in data['series']:
|
||||||
|
try:
|
||||||
|
parts = series.split(',', 1)
|
||||||
|
measurement = parts[0].strip()
|
||||||
|
tags_part = parts[1] if len(parts) > 1 else ''
|
||||||
|
|
||||||
|
# Raw tag parsing
|
||||||
|
tags = {}
|
||||||
|
for tag in tags_part.split(','):
|
||||||
|
if '=' in tag:
|
||||||
|
k, v = tag.split('=', 1)
|
||||||
|
tags[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
# Pobierz tylko tagi dostępne w measurement
|
||||||
|
tag_keys_query = client.query(f'SHOW TAG KEYS FROM "{measurement}"')
|
||||||
|
tag_keys = {row['tagKey'] for row in tag_keys_query.get_points()}
|
||||||
|
|
||||||
|
# Build WHERE clause tylko z tagów
|
||||||
|
where_parts = []
|
||||||
|
for k, v in tags.items():
|
||||||
|
if k not in tag_keys:
|
||||||
|
print(f"Pomijam '{k}', bo nie jest tagiem w '{measurement}'")
|
||||||
|
continue
|
||||||
|
v_escaped = v.replace("'", "\\'")
|
||||||
|
where_parts.append(f'"{k}" = \'{v_escaped}\'')
|
||||||
|
|
||||||
|
# Escape measurement for InfluxQL
|
||||||
|
measurement_escaped = measurement.replace('"', '\\"')
|
||||||
|
|
||||||
|
query = f'DROP SERIES FROM "{measurement_escaped}" WHERE {" AND ".join(where_parts)}'
|
||||||
|
print(f"DEBUG: Final query to run: {query}")
|
||||||
|
|
||||||
|
client.query(query)
|
||||||
|
deleted_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Błąd usuwania serii {series}: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return jsonify({'status': 'success', 'deleted': deleted_count})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/delete_range', methods=['POST'])
|
||||||
|
def delete_range():
|
||||||
|
data = request.json
|
||||||
|
db = data.get('db')
|
||||||
|
series = data.get('series')
|
||||||
|
time_from = data.get('from')
|
||||||
|
time_to = data.get('to')
|
||||||
|
|
||||||
|
if not all([db, series, time_from, time_to]):
|
||||||
|
return jsonify({'error': 'Brakuje wymaganych parametrów'}), 400
|
||||||
|
|
||||||
|
client.switch_database(db)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parsowanie measurementu i tagów z serii
|
||||||
|
parts = series.split(',', 1)
|
||||||
|
measurement = parts[0].strip()
|
||||||
|
tags_part = parts[1] if len(parts) > 1 else ''
|
||||||
|
|
||||||
|
tags = {}
|
||||||
|
for tag in tags_part.split(','):
|
||||||
|
if '=' in tag:
|
||||||
|
k, v = tag.split('=', 1)
|
||||||
|
tags[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
# Pobierz dozwolone tagi z InfluxDB
|
||||||
|
tag_keys_query = client.query(f'SHOW TAG KEYS FROM "{measurement}"')
|
||||||
|
tag_keys = {row['tagKey'] for row in tag_keys_query.get_points()}
|
||||||
|
|
||||||
|
# Buduj WHERE tylko z tagów i czasu
|
||||||
|
where_parts = [f"time >= '{time_from}'", f"time <= '{time_to}'"]
|
||||||
|
for k, v in tags.items():
|
||||||
|
if k not in tag_keys:
|
||||||
|
print(f"Pomijam '{k}', bo nie jest tagiem w '{measurement}'")
|
||||||
|
continue
|
||||||
|
v_escaped = v.replace("'", "\\'")
|
||||||
|
where_parts.append(f'"{k}" = \'{v_escaped}\'')
|
||||||
|
|
||||||
|
measurement_escaped = measurement.replace('"', '\\"')
|
||||||
|
query = f'DELETE FROM "{measurement_escaped}" WHERE {" AND ".join(where_parts)}'
|
||||||
|
|
||||||
|
print(f"DEBUG: DELETE query: {query}")
|
||||||
|
client.query(query)
|
||||||
|
return jsonify({'status': 'success'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='0.0.0.0', port=9999, debug=True)
|
9
config.example.py
Normal file
9
config.example.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from influxdb import InfluxDBClient
|
||||||
|
|
||||||
|
def get_influx_client():
|
||||||
|
return InfluxDBClient(
|
||||||
|
host='stats.mngmnt.r.local',
|
||||||
|
port=8086,
|
||||||
|
#username='admin',
|
||||||
|
#password='admin'
|
||||||
|
)
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
flask
|
||||||
|
flask-cors
|
||||||
|
influxdb
|
289
static/script.js
Normal file
289
static/script.js
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
let currentSeriesData = [];
|
||||||
|
let darkMode = true;
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
darkMode = !darkMode;
|
||||||
|
document.body.classList.toggle('bg-dark', darkMode);
|
||||||
|
document.body.classList.toggle('text-light', darkMode);
|
||||||
|
document.querySelector('.theme-switch').className = `bi ${darkMode ? 'bi-moon-stars-fill' : 'bi-sun-fill'} theme-switch`;
|
||||||
|
document.querySelectorAll('.form-select, .form-control').forEach(el => {
|
||||||
|
el.classList.toggle('bg-dark', darkMode);
|
||||||
|
el.classList.toggle('text-light', darkMode);
|
||||||
|
el.classList.toggle('border-secondary', darkMode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDatabases() {
|
||||||
|
const res = await fetch('/databases');
|
||||||
|
const dbs = await res.json();
|
||||||
|
const select = document.getElementById('db-select');
|
||||||
|
select.innerHTML = '<option value="">Wybierz bazę danych...</option>';
|
||||||
|
dbs.forEach(db => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = db;
|
||||||
|
option.textContent = db;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const dbParam = params.get('db');
|
||||||
|
if (dbParam && dbs.includes(dbParam)) {
|
||||||
|
select.value = dbParam;
|
||||||
|
fetchSeries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSeries() {
|
||||||
|
const db = document.getElementById('db-select').value;
|
||||||
|
if (!db) return;
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
params.set('db', db);
|
||||||
|
window.history.replaceState({}, '', `${location.pathname}?${params}`);
|
||||||
|
showMessage('Ładowanie serii...', 'info');
|
||||||
|
const res = await fetch(`/series?db=${db}`);
|
||||||
|
const series = await res.json();
|
||||||
|
currentSeriesData = series;
|
||||||
|
renderSeriesTable(series);
|
||||||
|
showMessage('', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSeriesTable(series) {
|
||||||
|
const container = document.getElementById('series-list');
|
||||||
|
container.innerHTML = '';
|
||||||
|
if (!Array.isArray(series) || series.length === 0) {
|
||||||
|
container.innerHTML = `<tr><td colspan="3" class="text-center">Brak serii w wybranej bazie danych</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseNames = new Map();
|
||||||
|
series.forEach(s => {
|
||||||
|
const entity = s.tags?.entity_id;
|
||||||
|
if (!entity) return;
|
||||||
|
const base = entity.replace(/_\d+$/, '');
|
||||||
|
if (baseNames.has(base)) baseNames.get(base).push(s.series);
|
||||||
|
else baseNames.set(base, [s.series]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const duplicates = new Set();
|
||||||
|
for (const [_, entries] of baseNames.entries()) {
|
||||||
|
if (entries.length > 1) entries.forEach(ser => duplicates.add(ser));
|
||||||
|
}
|
||||||
|
|
||||||
|
series.forEach((s, i) => {
|
||||||
|
const id = `series-${i}`;
|
||||||
|
const isDuplicate = duplicates.has(s.series);
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.className = isDuplicate ? 'table-warning' : '';
|
||||||
|
row.innerHTML = `
|
||||||
|
<td><input class="form-check-input" type="checkbox" value="${s.series}" id="${id}" onchange="updateCount()"></td>
|
||||||
|
<td class="series-item">
|
||||||
|
<label class="form-check-label" for="${id}">${s.series}</label>
|
||||||
|
${isDuplicate ? '<div class="text-warning small">Duplikat entity_id</div>' : ''}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-danger me-1" onclick="deleteSingleSeries('${s.series}')"><i class="bi bi-trash"></i></button>
|
||||||
|
<button class="btn btn-sm btn-outline-warning" onclick="openTimeDeleteModal('${s.series}')"><i class="bi bi-clock-history"></i></button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
container.appendChild(row);
|
||||||
|
});
|
||||||
|
updateCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterSeries() {
|
||||||
|
const term = document.getElementById('series-filter').value.toLowerCase();
|
||||||
|
const filtered = currentSeriesData.filter(s => s.series.toLowerCase().includes(term));
|
||||||
|
renderSeriesTable(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCount() {
|
||||||
|
const count = document.querySelectorAll('input[type="checkbox"]:checked').length;
|
||||||
|
document.getElementById('selected-count').innerText = `Zaznaczono: ${count}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
const checkboxes = document.querySelectorAll('input[type="checkbox"]:not(#select-all)');
|
||||||
|
const selectAll = document.getElementById('select-all').checked;
|
||||||
|
checkboxes.forEach(checkbox => checkbox.checked = selectAll);
|
||||||
|
updateCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessage(message, type) {
|
||||||
|
const el = document.getElementById('info-message');
|
||||||
|
el.textContent = message;
|
||||||
|
el.className = `alert alert-${type} ${message ? 'd-block' : 'd-none'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSelected() {
|
||||||
|
const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked:not(#select-all)');
|
||||||
|
if (checkboxes.length === 0) return showMessage('Nie zaznaczono żadnych serii', 'warning');
|
||||||
|
const series = Array.from(checkboxes).map(cb => cb.value);
|
||||||
|
const db = document.getElementById('db-select').value;
|
||||||
|
const preview = series.map(s => `DROP SERIES FROM "${s.split(',')[0]}" WHERE ...`).join('\n');
|
||||||
|
document.getElementById('confirmMessage').textContent = `Czy na pewno chcesz usunąć ${series.length} serii?`;
|
||||||
|
document.getElementById('queryPreview').textContent = preview;
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
|
||||||
|
modal.show();
|
||||||
|
document.getElementById('confirmDeleteBtn').onclick = async () => {
|
||||||
|
modal.hide();
|
||||||
|
await deleteSeries(series);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSingleSeries(seriesId) {
|
||||||
|
document.getElementById('confirmMessage').textContent = `Czy na pewno chcesz usunąć serię:`;
|
||||||
|
document.getElementById('queryPreview').textContent = `DROP SERIES FROM "${seriesId.split(',')[0]}" WHERE ...`;
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
|
||||||
|
modal.show();
|
||||||
|
document.getElementById('confirmDeleteBtn').onclick = async () => {
|
||||||
|
modal.hide();
|
||||||
|
await deleteSeries([seriesId]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteSeries(series) {
|
||||||
|
const db = document.getElementById('db-select').value;
|
||||||
|
showMessage('Usuwanie serii...', 'info');
|
||||||
|
const res = await fetch('/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ db, series })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.status === 'success') {
|
||||||
|
showMessage(`Usunięto ${data.deleted} serii`, 'success');
|
||||||
|
setTimeout(fetchSeries, 1000);
|
||||||
|
} else {
|
||||||
|
showMessage('Wystąpił błąd podczas usuwania serii', 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDatabases();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let currentSeriesForTimeDelete = null;
|
||||||
|
|
||||||
|
function openTimeDeleteModal(seriesId) {
|
||||||
|
currentSeriesForTimeDelete = seriesId;
|
||||||
|
document.getElementById('time-from').value = '';
|
||||||
|
document.getElementById('time-to').value = '';
|
||||||
|
document.getElementById('time-query-preview').textContent = '';
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('timeDeleteModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmTimeDelete() {
|
||||||
|
const from = document.getElementById('time-from').value;
|
||||||
|
const to = document.getElementById('time-to').value;
|
||||||
|
const db = document.getElementById('db-select').value;
|
||||||
|
const series = currentSeriesForTimeDelete;
|
||||||
|
|
||||||
|
if (!from || !to || !series || !db) {
|
||||||
|
showMessage('Uzupełnij wszystkie pola i wybierz bazę danych', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage('Usuwanie danych z serii...', 'info');
|
||||||
|
const res = await fetch('/delete_range', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ db, series, from, to })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.status === 'success') {
|
||||||
|
showMessage('Dane zostały usunięte', 'success');
|
||||||
|
setTimeout(fetchSeries, 1000);
|
||||||
|
} else {
|
||||||
|
showMessage(`Błąd: ${data.error}`, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setPredefinedRange(value) {
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const to = now.toISOString().slice(0, 16);
|
||||||
|
let fromDate = new Date(now);
|
||||||
|
|
||||||
|
if (value.endsWith('h')) {
|
||||||
|
fromDate.setHours(fromDate.getHours() - parseInt(value));
|
||||||
|
} else if (value.endsWith('d')) {
|
||||||
|
fromDate.setDate(fromDate.getDate() - parseInt(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = fromDate.toISOString().slice(0, 16);
|
||||||
|
document.getElementById('time-from').value = from;
|
||||||
|
document.getElementById('time-to').value = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setPredefinedRange(value) {
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const to = now.toISOString().slice(0, 16);
|
||||||
|
let fromDate = new Date(now);
|
||||||
|
|
||||||
|
if (value.endsWith('h')) {
|
||||||
|
fromDate.setHours(fromDate.getHours() - parseInt(value));
|
||||||
|
} else if (value.endsWith('d')) {
|
||||||
|
fromDate.setDate(fromDate.getDate() - parseInt(value));
|
||||||
|
} else if (value === 'before-current-year') {
|
||||||
|
const start = new Date(now.getFullYear(), 0, 1);
|
||||||
|
document.getElementById('time-from').value = '2000-01-01T00:00';
|
||||||
|
document.getElementById('time-to').value = start.toISOString().slice(0, 16);
|
||||||
|
return;
|
||||||
|
} else if (value === 'keep-2y') {
|
||||||
|
fromDate.setFullYear(fromDate.getFullYear() - 2);
|
||||||
|
} else if (value === 'keep-3y') {
|
||||||
|
fromDate.setFullYear(fromDate.getFullYear() - 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = fromDate.toISOString().slice(0, 16);
|
||||||
|
document.getElementById('time-from').value = from;
|
||||||
|
document.getElementById('time-to').value = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRangeForSelected() {
|
||||||
|
const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked:not(#select-all)');
|
||||||
|
if (checkboxes.length === 0) {
|
||||||
|
showMessage('Zaznacz przynajmniej jedną serię', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromInput = document.getElementById('time-from').value;
|
||||||
|
const toInput = document.getElementById('time-to').value;
|
||||||
|
const db = document.getElementById('db-select').value;
|
||||||
|
|
||||||
|
if (!fromInput || !toInput || !db) {
|
||||||
|
showMessage('Wybierz zakres czasu i bazę danych', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = new Date(fromInput).toISOString();
|
||||||
|
const to = new Date(toInput).toISOString();
|
||||||
|
const series = Array.from(checkboxes).map(cb => cb.value);
|
||||||
|
|
||||||
|
showMessage('Usuwanie danych z wielu serii...', 'info');
|
||||||
|
for (const s of series) {
|
||||||
|
await fetch('/delete_range', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ db, series: s, from, to })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage('Dane zostały usunięte', 'success');
|
||||||
|
setTimeout(fetchSeries, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTimeDeleteModalForMany() {
|
||||||
|
currentSeriesForTimeDelete = null; // żeby nie używać pojedynczej serii
|
||||||
|
document.getElementById('time-from').value = '';
|
||||||
|
document.getElementById('time-to').value = '';
|
||||||
|
document.getElementById('time-query-preview').textContent = '';
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('timeDeleteModal'));
|
||||||
|
modal.show();
|
||||||
|
}
|
19
static/styles.css
Normal file
19
static/styles.css
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
body {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-item {
|
||||||
|
font-family: monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
132
templates/index.html
Normal file
132
templates/index.html
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>InfluxDB Series Manager</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-night.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
</head>
|
||||||
|
<body class="bg-dark text-light">
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2 class="mb-0">InfluxDB Series Manager</h2>
|
||||||
|
<i class="bi bi-moon-stars-fill theme-switch" onclick="toggleTheme()"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-dark border-secondary mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="db-select" class="form-label">Wybierz bazę danych</label>
|
||||||
|
<select id="db-select" class="form-select bg-dark text-light border-secondary" onchange="fetchSeries()">
|
||||||
|
<option value="">Wybierz bazę danych...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="series-filter" class="form-label">Filtruj serie</label>
|
||||||
|
<input type="text" id="series-filter" class="form-control bg-dark text-light border-secondary" placeholder="Wpisz nazwę serii..." oninput="filterSeries()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<button class="btn btn-danger me-3" onclick="deleteSelected()">
|
||||||
|
<i class="bi bi-trash"></i> Usuń zaznaczone
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning me-3" onclick="openTimeDeleteModalForMany()">
|
||||||
|
<i class="bi bi-clock-history"></i> Usuń dane z zaznaczonych wg zakresu
|
||||||
|
</button>
|
||||||
|
<button style="display:none">
|
||||||
|
<i class="bi bi-trash"></i> Usuń zaznaczone
|
||||||
|
</button>
|
||||||
|
<span class="text-muted" id="selected-count">Zaznaczono: 0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info d-none" id="info-message"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-dark border-secondary">
|
||||||
|
<div class="card-body table-container">
|
||||||
|
<table class="table table-dark table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="50px"><input type="checkbox" class="form-check-input" id="select-all" onchange="toggleSelectAll()"></th>
|
||||||
|
<th>Seria</th>
|
||||||
|
<th width="100px">Akcje</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="series-list">
|
||||||
|
<tr><td colspan="3" class="text-center py-4">Wybierz bazę danych, aby załadować serie...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal potwierdzenia -->
|
||||||
|
<div class="modal fade" id="confirmModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content bg-dark text-light border border-secondary">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Potwierdzenie usunięcia</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="confirmMessage"></p>
|
||||||
|
<pre class="bg-black p-2 text-light small" id="queryPreview"></pre>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">Usuń</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Modal usuwania danych z zakresu czasu -->
|
||||||
|
<div class="modal fade" id="timeDeleteModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content bg-dark text-light border border-secondary">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Usuń dane z zakresu czasu</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Zakres czasu:</label>
|
||||||
|
<select class="form-select mb-2" onchange="setPredefinedRange(this.value)">
|
||||||
|
<option value="">-- wybierz --</option>
|
||||||
|
<option value="1h">Ostatnia godzina</option>
|
||||||
|
<option value="1d">Ostatni dzień</option>
|
||||||
|
<option value="7d">Ostatni tydzień</option>
|
||||||
|
<option value="30d">Ostatni miesiąc</option>
|
||||||
|
<option value="365d">Ostatni rok</option>
|
||||||
|
|
||||||
|
<option value="before-current-year">Wszystko poza bieżącym rokiem</option>
|
||||||
|
<option value="keep-2y">Pozostaw 2 ostatnie lata</option>
|
||||||
|
<option value="keep-3y">Pozostaw 3 ostatnie lata</option>
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<label for="time-from">Od (możesz też ustawić ręcznie):</label>
|
||||||
|
|
||||||
|
<input type="datetime-local" id="time-from" class="form-control mb-2">
|
||||||
|
<label for="time-to">Do:</label>
|
||||||
|
<input type="datetime-local" id="time-to" class="form-control mb-2">
|
||||||
|
<p class="small text-muted" id="time-query-preview"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
|
||||||
|
<button type="button" class="btn btn-danger me-2" onclick="confirmTimeDelete()">Usuń dane tej serii</button>
|
||||||
|
<button type="button" class="btn btn-warning" onclick="deleteRangeForSelected()">Usuń z zaznaczonych</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user