diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index 1af10d9..fe6fc83 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -1,4 +1,4 @@ -import { PanelOptions, TaskTableRowConfig, QueryTableRowConfig, DatasourceType } from '../types'; +import { PanelOptions, ExportTask, DashboardQuery, DatasourceType, ExportStatus } from '../types'; import { convertTimestampToDate, getDashboardUid } from '../../../utils'; import { CLOSE_ICON_BASE_64, DOWNLOAD_ICON_BASE_64, SELECT_ICON_BASE_64, UNSELECT_ICON_BASE_64 } from '../../../icons'; @@ -44,8 +44,8 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { const [dashboard, setDashboard] = useState(null); const [datasources, setDatasources] = useState(null); - const [tasks, setTasks] = useState(null); - const [queries, setQueries] = useState(null); + const [tasks, setTasks] = useState(null); + const [queries, setQueries] = useState(null); const [tasksDataFrame, setTasksDataFrame] = useState(null); const [queriesDataFrame, setQueriesDataFrame] = useState(null); @@ -77,7 +77,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { return; } dashboard.panels.forEach((panel: PanelModel) => { - const queries: QueryTableRowConfig[] = []; + const queries: DashboardQuery[] = []; // @ts-ignore // TODO: move plugin id to const @@ -124,8 +124,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { .then((tasks) => { setTasks(tasks); for (let task of tasks) { - // TODO: ExportStatus enum - if (task.status === 'exporting') { + if (task.progress?.status === ExportStatus.EXPORTING) { setTimeout(refresh, 1000); return; } @@ -155,18 +154,23 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { } async function onAddTaskClick(): Promise { - const selectedQueries = _.filter(queries, (query: QueryTableRowConfig) => query.selected); - // TODO: timerange picker + const selectedQueries = _.filter(queries, (query: DashboardQuery) => query.selected); const timerange: [number, number] = [selectedTimeRange.from.unix(), selectedTimeRange.to.unix()]; + + const task: ExportTask = { + // @ts-ignore + username: contextSrv.user.name, + timeRange: { + from: timerange[0] * 1000, + to: timerange[1] * 1000, + }, + queries: selectedQueries + } // TODO: move this function to API Service await queryApi('/task', { method: 'POST', data: { - from: timerange[0] * 1000, - to: timerange[1] * 1000, - // @ts-ignore - username: contextSrv.user.name, - tasks: selectedQueries, + task, url: window.location.toString(), }, }); @@ -181,7 +185,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { if (queries === null) { return; } - setQueries(queries.map((query: QueryTableRowConfig) => ({ ...query, selected: false }))); + setQueries(queries.map((query: DashboardQuery) => ({ ...query, selected: false }))); } function openDatasourceModal(): void { @@ -193,7 +197,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { unselectAllQueries(); } - function getDataFrameForQueriesTable(configs: QueryTableRowConfig[]): DataFrame { + function getDataFrameForQueriesTable(queries: DashboardQuery[]): DataFrame { const dataFrame = toDataFrame({ name: 'A', fields: [ @@ -201,8 +205,8 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { name: 'Select', type: FieldType.string, values: _.map( - configs, - (config) => `data:image/svg+xml;base64,${config.selected ? SELECT_ICON_BASE_64 : UNSELECT_ICON_BASE_64}` + queries, + (query) => `data:image/svg+xml;base64,${query.selected ? SELECT_ICON_BASE_64 : UNSELECT_ICON_BASE_64}` ), config: { custom: { @@ -222,17 +226,17 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { { name: 'Panel', type: FieldType.string, - values: _.map(configs, (config) => config.panel.title), + values: _.map(queries, (query) => query.panel.title), }, { name: 'RefId', type: FieldType.string, - values: _.map(configs, (config) => config.refId), + values: _.map(queries, (query) => query.refId), }, { name: 'Datasource', type: FieldType.string, - values: _.map(configs, (config) => config.datasource.name), + values: _.map(queries, (query) => query.datasource.name), }, ], }); @@ -249,44 +253,49 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { return dataFrames[0]; } - function getDataFrameForTaskTable(configs: TaskTableRowConfig[]): DataFrame { + function getDataFrameForTaskTable(tasks: ExportTask[]): DataFrame { const dataFrame = toDataFrame({ name: 'A', fields: [ { name: 'Time', type: FieldType.number, - values: _.map(configs, (config) => convertTimestampToDate(config.timestamp)), + values: _.map(tasks, (task) => convertTimestampToDate(task.progress?.time)), }, { name: 'User', type: FieldType.string, - values: _.map(configs, (config) => config.username), + values: _.map(tasks, (task) => task.username), }, { name: 'Datasource', type: FieldType.string, - values: _.map(configs, (config) => getDatasourceByUid(config.datasourceRef?.uid)?.name), + values: _.map(tasks, (task) => task.queries.map(query => query.datasource?.name).join(',')), }, { name: 'Exported Rows', type: FieldType.number, - values: _.map(configs, (config) => config.rowsCount), + values: _.map(tasks, (task) => task.progress?.exportedRowsCount), }, { name: 'Progress', type: FieldType.string, - values: _.map(configs, (config) => `${(config.progress * 100).toFixed(0)}%`), + values: _.map(tasks, (task) => `${((task.progress?.progress || 0) * 100).toFixed(0)}%`), }, { name: 'Status', type: FieldType.string, - values: _.map(configs, (config) => config.status), + values: _.map(tasks, (task) => task.progress?.status), + }, + { + name: 'Error', + type: FieldType.string, + values: _.map(tasks, (task) => task.progress?.errorMessage || '-'), }, { name: 'Download CSV', type: FieldType.string, - values: _.map(configs, () => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`), + values: _.map(tasks, () => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`), config: { custom: { filterable: false, @@ -305,7 +314,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { { name: 'Delete task', type: FieldType.string, - values: _.map(configs, () => `data:image/png;base64,${CLOSE_ICON_BASE_64}`), + values: _.map(tasks, () => `data:image/png;base64,${CLOSE_ICON_BASE_64}`), config: { custom: { filterable: false, @@ -340,7 +349,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { const rowIndex = e.origin.rowIndex; const task = _.find(tasks, (task, idx) => idx === rowIndex); - await deleteTask(task?.filename); + await deleteTask(task?.id); const filteredTasks = _.filter(tasks, (task, idx) => idx !== rowIndex); setTasks(filteredTasks); @@ -349,7 +358,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { function onDownloadClick(e: DataLinkClickEvent): void { const rowIndex = e.origin.rowIndex; const task = _.find(tasks, (task, idx) => idx === rowIndex); - getStaticFile(task?.filename); + getStaticFile(task?.id); } function onDatasourceSelectClick(e: DataLinkClickEvent): void { @@ -409,7 +418,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) { aria-label="Add task button" onClick={onAddTaskClick} // TODO: move to function - disabled={!queries?.filter((query: QueryTableRowConfig) => query.selected)?.length} + disabled={!queries?.filter((query: DashboardQuery) => query.selected)?.length} > Add Task diff --git a/src/panels/corpglory-dataexporter-panel/types.ts b/src/panels/corpglory-dataexporter-panel/types.ts index c83ef92..915a58d 100644 --- a/src/panels/corpglory-dataexporter-panel/types.ts +++ b/src/panels/corpglory-dataexporter-panel/types.ts @@ -1,23 +1,7 @@ -import { DataQuery, DataSourceRef, DataSourceSettings, PanelModel } from '@grafana/data'; +import { DataQuery, DataSourceSettings, PanelModel } from '@grafana/data'; export interface PanelOptions {} -export type TaskTableRowConfig = { - timestamp: number; - username: string; - datasourceRef: DataSourceRef; - rowsCount: number; - progress: number; - status: string; - filename?: string; -}; - -export type QueryTableRowConfig = Omit & { - selected: boolean; - panel: PanelModel; - datasource: DataSourceSettings; -}; - export enum DatasourceType { INFLUXDB = 'influxdb', GRAPHITE = 'graphite', @@ -26,3 +10,34 @@ export enum DatasourceType { ELASTICSEARCH = 'elasticsearch', MYSQL = 'mysql', } + +export enum ExportStatus { + EXPORTING = 'exporting', + FINISHED = 'finished', + ERROR = 'error', +} + +export type ExportProgress = { + time: number; + exportedRowsCount: number; + progress: number; + status: ExportStatus; + errorMessage?: string; +}; + +export type ExportTask = { + username: string; + queries: DashboardQuery[]; + timeRange: { + from: number; + to: number; + }; + progress?: ExportProgress; + id?: string; +}; + +export type DashboardQuery = DataQuery & { + selected: boolean; + panel: PanelModel; + datasource: DataSourceSettings; +}; diff --git a/src/services/api_service.ts b/src/services/api_service.ts index a803327..d12acd0 100644 --- a/src/services/api_service.ts +++ b/src/services/api_service.ts @@ -1,4 +1,4 @@ -import { TaskTableRowConfig } from '../panels/corpglory-dataexporter-panel/types'; +import { ExportTask } from '../panels/corpglory-dataexporter-panel/types'; import axios from 'axios'; import * as _ from 'lodash'; @@ -43,24 +43,24 @@ export const queryApi = async (path: string, config: RequestConfig) => return response.data as RT; }; -export async function getTasks(): Promise { - return queryApi('/task', {}); +export async function getTasks(): Promise { + return queryApi('/task', {}); } -export async function deleteTask(filename?: string): Promise { - if (_.isEmpty(filename)) { - console.warn(`can't delete task without filename`); +export async function deleteTask(taskId?: string): Promise { + if (_.isEmpty(taskId)) { + console.warn(`can't delete task without taskId`); return; } - await queryApi('/task', { method: 'DELETE', data: { filename } }); + await queryApi('/task', { method: 'DELETE', data: { taskId } }); } -export async function getStaticFile(filename?: string): Promise { - if (_.isEmpty(filename)) { - console.warn(`can't download file without name`); +export async function getStaticFile(taskId?: string): Promise { + if (_.isEmpty(taskId)) { + console.warn(`can't download file without taskId`); return; } - const respData = await queryApi(`/static/${filename}.csv`, {}); + const respData = await queryApi(`/static/${taskId}.csv`, {}); // TODO: check if resp exists // create file link in browser's memory const href = URL.createObjectURL(new Blob([respData], { type: 'text/csv' })); @@ -68,7 +68,7 @@ export async function getStaticFile(filename?: string): Promise { // create "a" HTML element with href to file & click const link = document.createElement('a'); link.href = href; - link.setAttribute('download', `${filename}.csv`); + link.setAttribute('download', `${taskId}.csv`); document.body.appendChild(link); link.click(); diff --git a/src/utils/index.ts b/src/utils/index.ts index 43ac013..83e9eac 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -15,7 +15,7 @@ export function getDashboardUid(url: string): string { } } -export function convertTimestampToDate(timestamp: number): string { +export function convertTimestampToDate(timestamp?: number): string { const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', @@ -24,5 +24,7 @@ export function convertTimestampToDate(timestamp: number): string { minute: 'numeric', second: 'numeric', }; - return new Date(timestamp).toLocaleString('en-GB', options); + return timestamp ? + new Date(timestamp).toLocaleString('en-GB', options): + '-'; }