import { PanelOptions, TaskTableRowConfig, DatasourceTableRowConfig } from '../types'; import { getBackendSrv } from '@grafana/runtime'; import { makeRequest } from 'services/network_service'; import { Table, Button, HorizontalGroup, Modal, LoadingPlaceholder } from '@grafana/ui'; import { PanelProps, toDataFrame, FieldType, applyFieldOverrides, createTheme, DataFrame, DataLinkClickEvent, } from '@grafana/data'; import React, { useState, useEffect } from 'react'; import * as _ from 'lodash'; interface Props extends PanelProps {} export function Panel({ options, data, width, height, timeRange, onChangeTimeRange }: Props) { const [tasks, setTasks] = useState(null); const [tasksDataFrame, setTasksDataFrame] = useState(null); useEffect(() => { if (tasks === null) { return; } const dataFrame = getDataFrameForTaskTable(tasks, setTasks); setTasksDataFrame(dataFrame); }, [tasks]); useEffect(() => { // TODO: move this function to Network Service async function getTasks(): Promise { return makeRequest('/task', {}); } getTasks() .then((tasks) => setTasks(tasks)) .catch((err) => console.error(err)); }, []); const datasourceConfigs: DatasourceTableRowConfig[] = []; const [datasources, setDatasources] = useState(datasourceConfigs); const datasourceDataFrame = getDataFrameForDatasourceTable(datasources, setDatasources); const [isModalOpen, setModalVisibility] = useState(false); const timestampRange: [number, number] = [timeRange.from.unix(), timeRange.to.unix()]; const backendSrv = getBackendSrv(); // @ts-ignore backendSrv.getInspectorStream().subscribe({ next: (resp: any) => { const queries = resp?.config?.data?.queries; if (_.isEmpty(queries)) { return; } const datasource = queries[0].datasource.type; const refId = queries[0].refId; const rawSql = queries[0].rawSql; const uid = queries[0].datasource.uid; // TODO: it works only with sql (rawSql will be empty in prometheus) const isDatasourceExist = _.some( datasources, (ds: DatasourceTableRowConfig) => ds.datasource === datasource && ds.refId === refId && _.isEqual(ds.rawSql, rawSql) && ds.uid === uid ); if (isDatasourceExist) { return; } const newConfig = createDatasourceConfig({ datasource, refId, rawSql, uid }, timestampRange); setDatasources([...datasources, newConfig]); }, }); function onAddTaskClick(): void { const selectedDatasources = _.filter(datasources, (datasource: DatasourceTableRowConfig) => datasource.select); const newTasks = _.map(selectedDatasources, (datasource: DatasourceTableRowConfig) => createTaskFromDatasource(datasource) ); if (_.isEmpty(tasks)) { setTasks([...newTasks]); } else { setTasks([...(tasks as TaskTableRowConfig[]), ...newTasks]); } onCloseModal(); unselectAllDatasources(); } function unselectAllDatasources(): void { const updatedDatasources = _.clone(datasources); for (const ds of updatedDatasources) { ds.select = false; } setDatasources(updatedDatasources); } function openDatasourceModal(): void { setModalVisibility(true); } function onCloseModal(): void { setModalVisibility(false); unselectAllDatasources(); } return (
{tasksDataFrame === null ? ( // TODO: if datasource responds with error, display the error ) : (
)} ); } function getDataFrameForTaskTable(configs: TaskTableRowConfig[], setTasks: any): DataFrame { const dataFrame = toDataFrame({ name: 'A', fields: [ { name: 'Time', type: FieldType.number, values: _.map(configs, (config) => convertTimestampToDate(config.timestamp)), }, { name: 'User', type: FieldType.string, values: _.map(configs, (config) => config.user), }, { name: 'Datasource', type: FieldType.string, values: _.map(configs, (config) => config.datasource), }, { name: 'Exported Rows', type: FieldType.number, values: _.map(configs, (config) => config.rowsCount), }, { name: 'Progress', type: FieldType.string, values: _.map(configs, (config) => `${config.progress}%`), }, { name: 'Status', type: FieldType.string, values: _.map(configs, (config) => config.status), }, { name: 'Download CSV', type: FieldType.string, values: _.map(configs, (config) => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`), config: { custom: { filterable: false, displayMode: 'image', }, links: [ { targetBlank: false, title: 'CSV link', url: 'https://chartwerk.io/', }, ], }, }, { name: 'Delete task', type: FieldType.string, values: _.map(configs, (config) => `data:image/png;base64,${CLOSE_ICON_BASE_64}`), config: { custom: { filterable: false, displayMode: 'image', }, links: [ { targetBlank: false, title: 'Delete', url: '#', onClick: (event: DataLinkClickEvent) => onDeleteClick(event, configs, setTasks), }, ], }, }, ], }); const dataFrames = applyFieldOverrides({ data: [dataFrame], fieldConfig: { overrides: [], defaults: {}, }, theme: createTheme(), replaceVariables: (value: string) => value, }); return dataFrames[0]; } function getDataFrameForDatasourceTable(configs: DatasourceTableRowConfig[], setDatasources: any): DataFrame { const dataFrame = toDataFrame({ name: 'A', fields: [ { name: 'Select', type: FieldType.string, values: _.map( configs, (config) => `data:image/svg+xml;base64,${config.select ? SELECT_ICON_BASE_64 : UNSELECT_ICON_BASE_64}` ), config: { custom: { filterable: false, displayMode: 'image', }, links: [ { targetBlank: false, title: 'Select', url: '#', onClick: (event: DataLinkClickEvent) => onDatasourceSelectClick(event, configs, setDatasources), }, ], }, }, { name: 'Panel', type: FieldType.string, values: _.map(configs, (config) => config.panel), }, { name: 'RefId', type: FieldType.string, values: _.map(configs, (config) => config.refId), }, { name: 'Datasource', type: FieldType.string, values: _.map(configs, (config) => config.datasource), }, ], }); const dataFrames = applyFieldOverrides({ data: [dataFrame], fieldConfig: { overrides: [], defaults: {}, }, theme: createTheme(), replaceVariables: (value: string) => value, }); return dataFrames[0]; } function onDeleteClick(e: DataLinkClickEvent, tasks: TaskTableRowConfig[], setTasks: any): void { const rowIndex = e.origin.rowIndex; const filteredTasks = _.filter(tasks, (task, idx) => idx !== rowIndex); setTasks(filteredTasks); } function onDatasourceSelectClick( e: DataLinkClickEvent, datasources: DatasourceTableRowConfig[], setDatasources: any ): void { const rowIndex = e.origin.rowIndex; const updatedDatasources = _.clone(datasources); updatedDatasources[rowIndex].select = !updatedDatasources[rowIndex].select; setDatasources(updatedDatasources); } function createTaskFromDatasource(config: DatasourceTableRowConfig): TaskTableRowConfig { return { timestamp: Date.now(), user: 'admin', datasource: config.datasource, rowsCount: 3214, progress: 100, status: 'finished', }; } function createDatasourceConfig(obj: any, timerange: [number, number]): DatasourceTableRowConfig { return { select: false, panel: 'Panel', refId: obj.refId, datasource: obj.datasource, rawSql: obj.rawSql, uid: obj.uid, timerange, }; } function convertTimestampToDate(timestamp: number): string { const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', }; return new Date(timestamp).toLocaleString('en-GB', options); } const CLOSE_ICON_BASE_64 = 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACJSURBVHgB7ZPRCcAgDESv0lWc0zhC53OIjmAVUpCSamL76YEoyfFIiAGWfldK6SwnKHyhep9xJ3iPcqgH5RyxV1VlBWYJypXVHMEiCaqBbRhAy3W3B76j954wq6ZSVZsOY+WXt6i9l2ymGTlUq0VpOcIqaQC96Zth01DN1zBBefVI4SNp9Za+6wLcH6DKFrfpxgAAAABJRU5ErkJggg=='; const DOWNLOAD_ICON_BASE_64 = 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADlSURBVHgB3ZPNDYJAEIVnN96lBA7A2RIsATuQiiwBSrAD7MAjCZBACXqFwPomMRtEwMVwgZeQhfn5mNnMEG1CaZoeiqKwTWJ3JkFCiEtVVU+8+r9iJRlKSrk3iqOFtWJglmXHf3yDwCRJbBxxnudh34cRYluMMbKGcgWNCIlnjEuIJ1JK2WzDWeKb7YHjONEsYBf6kTABY+mWuYX+3Xiex9UFU7D3FllfwLqueQvi/h8ZCtBprDLY703T6A0yWj2ArmSoxedQV4jSSz5xj4pmCi0/NKfrwNz5bdtaNENciOu6N1qNXhzZXHMb9Q+nAAAAAElFTkSuQmCC'; const UNSELECT_ICON_BASE_64 = 'PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgcng9IjEuNSIgZmlsbD0iIzExMTIxNiIgc3Ryb2tlPSIjMkQyRTM0Ii8+Cjwvc3ZnPgo='; const SELECT_ICON_BASE_64 = 'PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiByeD0iMiIgZmlsbD0iIzRBNzFEMiIvPgo8cGF0aCBkPSJNNS45ODI2NCAxMi41MjE3QzUuNzczOTUgMTIuNTIxNyA1LjU4MjY0IDEyLjQ1MjIgNS40MjYxMiAxMi4yOTU2TDEuOTY1MjUgOC44MzQ3NkMxLjY1MjIxIDguNTIxNzIgMS42NTIyMSA4LjAzNDc2IDEuOTY1MjUgNy43MjE3MkMyLjI3ODI5IDcuNDA4NjcgMi43NjUyNSA3LjQwODY3IDMuMDc4MjkgNy43MjE3Mkw2LjAwMDAzIDEwLjYyNjFMMTIuOTM5MiAzLjcwNDMzQzEzLjI1MjIgMy4zOTEyOCAxMy43MzkyIDMuMzkxMjggMTQuMDUyMiAzLjcwNDMzQzE0LjM2NTMgNC4wMTczNyAxNC4zNjUzIDQuNTA0MzMgMTQuMDUyMiA0LjgxNzM3TDYuNTU2NTYgMTIuMjk1NkM2LjM4MjY0IDEyLjQ1MjIgNi4xOTEzNCAxMi41MjE3IDUuOTgyNjQgMTIuNTIxN1YxMi41MjE3WiIgZmlsbD0iI0ZFRkZGRiIvPgo8L3N2Zz4K';