import { PanelOptions, ExportTask, DashboardQuery, DatasourceType, ExportStatus, PanelStatus, Dashboard, } from '../types'; import { convertTimestampToDate, convertTimeZoneTypeToName, getCurrentDashboardUid } from '../../../utils'; import { CLOSE_ICON_BASE_64, DOWNLOADING_ICON_BASE_64, OPTIONS_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, timeZone }: Props) { const [dashboard, setDashboard] = useState(null); const [datasources, setDatasources] = useState(null); const [tasks, setTasks] = useState(null); const [queries, setQueries] = useState(null); const [taskIdBeingDownloaded, setTaskIdBeingDownloaded] = 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); function setPanelStatusWithValidate(status: PanelStatus): void { if (panelStatus === PanelStatus.PERMISSION_ERROR) { return; } return setPanelStatus(status); } const [errorMessage, setErrorMessage] = useState(null); const timeZoneName = convertTimeZoneTypeToName(timeZone); if (contextSrv.user.orgRole !== OrgRole.Admin) { setPanelStatusWithValidate(PanelStatus.PERMISSION_ERROR); } useEffect(() => { async function getCurrentDashboard(): Promise { const currentDashboardUid = getCurrentDashboardUid(); 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 (tasks === null) { return; } const dataFrame = getDataFrameForTaskTable(tasks); setTasksDataFrame(dataFrame); }, [tasks, taskIdBeingDownloaded]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(fetchTasks, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (queries === null) { return; } setQueriesDataFrame(getDataFrameForQueriesTable(queries)); }, [queries]); // eslint-disable-line react-hooks/exhaustive-deps async function updateQueries(): Promise { if (!dashboard || !datasources) { console.warn(`Can't update queries if there is no dashboard or datasources`); return; } const queries: DashboardQuery[] = []; dashboard.panels?.forEach((panel: PanelModel) => { // @ts-ignore if (panel.type === PANEL_ID) { return; } if (!_.includes(_.values(DatasourceType), panel.datasource?.type)) { return; } panel.targets?.forEach((target: DataQuery) => { const datasource = getDatasourceByUid(target.datasource?.uid); if (!datasource) { return; } queries.push({ selected: false, target, panel, datasource }); }); }); setQueries(queries); } function fetchTasks(): void { const dashboardUid = getCurrentDashboardUid(); getTasks(dashboardUid) .then((tasks) => { setTasks(tasks); setPanelStatusWithValidate(PanelStatus.OK); for (let task of tasks) { if (task.progress?.status === ExportStatus.EXPORTING) { setTimeout(fetchTasks, 1000); return; } } }) .catch((err) => { setPanelStatusWithValidate(PanelStatus.DATASOURCE_ERROR); console.error('some error', err); setErrorMessage(`${err.name}: ${err.message}`); }); } eventBus.subscribe(RefreshEvent, fetchTasks); 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(timeZoneName: string): Promise { const selectedQueries = _.filter(queries, (query: DashboardQuery) => query.selected); const timerange: [number, number] = [selectedTimeRange.from.unix(), selectedTimeRange.to.unix()]; const dashboardUid = getCurrentDashboardUid(); const task: ExportTask = { dashboardUid, // @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(), timeZoneName, }, }); fetchTasks(); onCloseModal(); unselectAllQueries(); } function unselectAllQueries(): void { if (queries === null) { return; } setQueries(queries.map((query: DashboardQuery) => ({ ...query, selected: false }))); } function openDatasourceModal(): void { updateQueries(); 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) => (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.target.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 sortedTasks = _.orderBy(tasks, (task) => task.progress?.time, 'desc'); const dataFrame = toDataFrame({ name: 'A', fields: [ { name: 'Status Updated At', type: FieldType.number, values: _.map(sortedTasks, (task) => convertTimestampToDate(task.progress?.time)), }, { name: 'From', type: FieldType.number, values: _.map(sortedTasks, (task) => convertTimestampToDate(task.timeRange.from)), }, { name: 'To', type: FieldType.number, values: _.map(sortedTasks, (task) => convertTimestampToDate(task.timeRange.to)), }, { name: 'User', type: FieldType.string, values: _.map(sortedTasks, (task) => task.username), }, { name: 'Datasource', type: FieldType.string, values: _.map(sortedTasks, (task) => task.queries.map((query) => query.datasource?.name).join(',')), }, { name: 'Exported Rows', type: FieldType.number, values: _.map(sortedTasks, (task) => task.progress?.exportedRowsCount), }, { name: 'Progress', type: FieldType.string, values: _.map(sortedTasks, (task) => `${((task.progress?.progress || 0) * 100).toFixed(0)}%`), }, { name: 'Status', type: FieldType.string, values: _.map(sortedTasks, (task) => task.progress?.status), }, { name: 'Error', type: FieldType.string, values: _.map(sortedTasks, (task) => task.progress?.errorMessage || '-'), }, { name: 'Actions', type: FieldType.string, values: _.map(sortedTasks, (task) => { switch (task.progress?.status) { case ExportStatus.FINISHED: if (task.id === taskIdBeingDownloaded) { return DOWNLOADING_ICON_BASE_64; } else { return OPTIONS_ICON_BASE_64; } case ExportStatus.ERROR: return CLOSE_ICON_BASE_64; case ExportStatus.EXPORTING: return ''; default: throw new Error(`Unknown exporting status: ${task.progress?.status}`); } }), config: { custom: { filterable: false, displayMode: 'image', }, }, }, ], }); const dataFrames = applyFieldOverrides({ data: [dataFrame], fieldConfig: { overrides: [], defaults: {}, }, theme: createTheme(), replaceVariables: (value: string) => value, }); // @ts-expect-error dataFrames[0].fields[9].getLinks = (cell: { valueRowIndex: number }) => { const rowIndex = cell.valueRowIndex; const task = _.find(sortedTasks, (task, idx) => idx === rowIndex); if (!task) { return; } switch (task.progress?.status) { case ExportStatus.FINISHED: return [ { targetBlank: false, title: 'Download', url: '#', onClick: () => onDownloadClick(task), }, { targetBlank: false, title: 'Delete', url: '#', onClick: () => onDeleteClick(task), }, ]; case ExportStatus.ERROR: return [ { targetBlank: false, title: 'Delete', url: '#', onClick: () => onDeleteClick(task), }, ]; case ExportStatus.EXPORTING: return []; default: throw new Error(`Unknown exporting status: ${task.progress?.status}`); } }; return dataFrames[0]; } async function onDeleteClick(taskToDelete: ExportTask): Promise { await deleteTask(taskToDelete?.id); const filteredTasks = _.filter(tasks, (task) => task.id !== taskToDelete.id); setTasks(filteredTasks); } async function onDownloadClick(task: ExportTask): Promise { setTaskIdBeingDownloaded(task.id as string); await getStaticFile(task?.id); setTaskIdBeingDownloaded(null); } 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.

If you have not setup the plugin click here to configure DataExporter.

{errorMessage}

); const permissionErrorDiv = (

Permission Error.

DataExporter panel available only for Admins.
); const mainDiv = (
{queriesDataFrame === null ? (

There are no queries to export.

) : (
{ 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; } `, customLink: css` color: #6e9fff; margin: 0 4px; &:hover { text-decoration: underline; } `, });