import { PanelOptions, ExportTask, DashboardQuery, DatasourceType, ExportStatus, PanelStatus } 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'; import { deleteTask, getStaticFile, getTasks, queryApi } from '../../../services/api_service'; import { getDashboardByUid, getDatasources } from '../../../services/grafana_backend_service'; import { contextSrv } from 'grafana/app/core/core'; import { css } from '@emotion/css'; import { Table, Button, HorizontalGroup, VerticalGroup, Modal, LoadingPlaceholder, TimeRangeInput, useStyles2, } from '@grafana/ui'; import { PanelProps, toDataFrame, FieldType, applyFieldOverrides, createTheme, DataFrame, DataLinkClickEvent, PanelModel, DataQuery, DataSourceSettings, TimeRange, OrgRole, } from '@grafana/data'; import { RefreshEvent } from '@grafana/runtime'; import React, { useState, useEffect } from 'react'; import * as _ from 'lodash'; const PANEL_ID = 'corpglory-dataexporter-panel'; const APP_ID = 'corpglory-dataexporter-app'; interface Props extends PanelProps {} export function Panel({ width, height, timeRange, eventBus }: Props) { // TODO: Dashboard type const [dashboard, setDashboard] = useState(null); const [datasources, setDatasources] = useState(null); const [tasks, setTasks] = useState(null); const [queries, setQueries] = useState(null); const [tasksDataFrame, setTasksDataFrame] = useState(null); const [queriesDataFrame, setQueriesDataFrame] = useState(null); const [isModalOpen, setModalVisibility] = useState(false); const [selectedTimeRange, setTimeRange] = useState(timeRange); const [panelStatus, setPanelStatus] = useState(PanelStatus.LOADING); if (contextSrv.user.orgRole !== OrgRole.Admin) { // TODO: it shouldn't be overriten setPanelStatus(PanelStatus.PERMISSION_ERROR); } useEffect(() => { async function getCurrentDashboard(): Promise { const currentDashboardUid = getDashboardUid(window.location.toString()); return getDashboardByUid(currentDashboardUid); } getCurrentDashboard() .then((dash) => setDashboard(dash.dashboard)) .catch((err) => console.error(err)); }, []); useEffect(() => { getDatasources() .then((datasources) => setDatasources(datasources)) .catch((err) => console.error(err)); }, []); useEffect(() => { if (!dashboard || !datasources) { return; } dashboard.panels.forEach((panel: PanelModel) => { const queries: DashboardQuery[] = []; // @ts-ignore if (panel.type === PANEL_ID) { return; } if (!_.includes(_.values(DatasourceType), panel.datasource?.type)) { return; } panel.targets?.forEach((target: DataQuery) => { console.log('uid', target.datasource?.uid); const datasource = getDatasourceByUid(target.datasource?.uid); if (!datasource) { return; } queries.push({ ...target, selected: false, panel, datasource }); }); setQueries(queries); }); }, [dashboard, datasources]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (tasks === null) { return; } const dataFrame = getDataFrameForTaskTable(tasks); setTasksDataFrame(dataFrame); }, [tasks]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(refresh, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (queries === null) { return; } setQueriesDataFrame(getDataFrameForQueriesTable(queries)); }, [queries]); // eslint-disable-line react-hooks/exhaustive-deps function refresh(): void { getTasks() .then((tasks) => { setTasks(tasks); setPanelStatus(PanelStatus.OK); for (let task of tasks) { if (task.progress?.status === ExportStatus.EXPORTING) { setTimeout(refresh, 1000); return; } } }) .catch((err) => { setPanelStatus(PanelStatus.DATASOURCE_ERROR); console.error('some error', err); }); } eventBus.subscribe(RefreshEvent, refresh); function getDatasourceByUid(uid?: string): DataSourceSettings | undefined { if (_.isNil(uid)) { console.warn(`uid is required to get datasource`); return undefined; } if (datasources === null) { console.warn(`there is no datasources yet`); return undefined; } const datasource = _.find(datasources, (datasource: DataSourceSettings) => datasource.uid === uid); if (!datasource) { console.warn(`can't find datasource "${uid}"`); } return datasource; } async function onAddTaskClick(): Promise { 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: { task, url: window.location.toString(), }, }); refresh(); onCloseModal(); unselectAllQueries(); } function unselectAllQueries(): void { if (queries === null) { return; } setQueries(queries.map((query: DashboardQuery) => ({ ...query, selected: false }))); } function openDatasourceModal(): void { setTimeRange(timeRange); setModalVisibility(true); } function onCloseModal(): void { setModalVisibility(false); unselectAllQueries(); } function getDataFrameForQueriesTable(queries: DashboardQuery[]): DataFrame { const dataFrame = toDataFrame({ name: 'A', fields: [ { name: 'Select', type: FieldType.string, values: _.map( queries, (query) => `data:image/svg+xml;base64,${query.selected ? 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), }, ], }, }, { name: 'Panel', type: FieldType.string, values: _.map(queries, (query) => query.panel.title), }, { name: 'RefId', type: FieldType.string, values: _.map(queries, (query) => query.refId), }, { name: 'Datasource', type: FieldType.string, values: _.map(queries, (query) => query.datasource.name), }, ], }); const dataFrames = applyFieldOverrides({ data: [dataFrame], fieldConfig: { overrides: [], defaults: {}, }, theme: createTheme(), replaceVariables: (value: string) => value, }); return dataFrames[0]; } function getDataFrameForTaskTable(tasks: ExportTask[]): DataFrame { const dataFrame = toDataFrame({ name: 'A', fields: [ { name: 'Time', type: FieldType.number, values: _.map(tasks, (task) => convertTimestampToDate(task.progress?.time)), }, { name: 'User', type: FieldType.string, values: _.map(tasks, (task) => task.username), }, { name: 'Datasource', type: FieldType.string, values: _.map(tasks, (task) => task.queries.map((query) => query.datasource?.name).join(',')), }, { name: 'Exported Rows', type: FieldType.number, values: _.map(tasks, (task) => task.progress?.exportedRowsCount), }, { name: 'Progress', type: FieldType.string, values: _.map(tasks, (task) => `${((task.progress?.progress || 0) * 100).toFixed(0)}%`), }, { name: 'Status', type: FieldType.string, 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(tasks, () => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`), config: { custom: { filterable: false, displayMode: 'image', }, links: [ { targetBlank: false, title: 'Download', url: '#', onClick: (event: DataLinkClickEvent) => onDownloadClick(event), }, ], }, }, { name: 'Delete task', type: FieldType.string, values: _.map(tasks, () => `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), }, ], }, }, ], }); const dataFrames = applyFieldOverrides({ data: [dataFrame], fieldConfig: { overrides: [], defaults: {}, }, theme: createTheme(), replaceVariables: (value: string) => value, }); return dataFrames[0]; } async function onDeleteClick(e: DataLinkClickEvent): Promise { const rowIndex = e.origin.rowIndex; const task = _.find(tasks, (task, idx) => idx === rowIndex); await deleteTask(task?.id); const filteredTasks = _.filter(tasks, (task, idx) => idx !== rowIndex); setTasks(filteredTasks); } function onDownloadClick(e: DataLinkClickEvent): void { const rowIndex = e.origin.rowIndex; const task = _.find(tasks, (task, idx) => idx === rowIndex); getStaticFile(task?.id); } function onDatasourceSelectClick(e: DataLinkClickEvent): void { if (queries === null) { return; } const rowIndex = e.origin.rowIndex; const updatedQueries = _.clone(queries); updatedQueries[rowIndex].selected = !updatedQueries[rowIndex].selected; setQueries(updatedQueries); } const styles = useStyles2(getStyles); const loadingDiv = ; // TODO: add styles const datasourceErrorDiv = (

Datasource is unavailable.

Click here to configure DataExporter.
{/* TODO: display error message? */}
); const permissionErrorDiv = (

Permission Error.

DataExporter panel availabel only for Admins
); const mainDiv = (
{queriesDataFrame === null ? ( // TODO: if datasource responds with error, display the error ) : (
{ setTimeRange(newTimeRange); }} />
)} ); function renderSwitch(panelStatus: PanelStatus): JSX.Element { switch (panelStatus) { case PanelStatus.LOADING: return loadingDiv; case PanelStatus.DATASOURCE_ERROR: return datasourceErrorDiv; case PanelStatus.PERMISSION_ERROR: return permissionErrorDiv; case PanelStatus.OK: return mainDiv; default: return datasourceErrorDiv; } } return
{renderSwitch(panelStatus)}
; } const getStyles = () => ({ calendarModal: css` section { position: fixed; top: 20%; z-index: 1061; } `, });