From f0fe98fa5790683887a34c126d74ec3d3c701f8d Mon Sep 17 00:00:00 2001 From: rozetko Date: Thu, 19 Jan 2023 23:49:19 +0300 Subject: [PATCH 01/13] auto api key configuration --- .../PluginConfigPage/PluginConfigPage.tsx | 56 ++++++++++--------- .../parts/ConfigurationForm/index.tsx | 4 +- src/plugin_state.ts | 21 ++++--- src/types.ts | 2 +- 4 files changed, 42 insertions(+), 41 deletions(-) diff --git a/src/components/PluginConfigPage/PluginConfigPage.tsx b/src/components/PluginConfigPage/PluginConfigPage.tsx index d352dfa..19dc254 100644 --- a/src/components/PluginConfigPage/PluginConfigPage.tsx +++ b/src/components/PluginConfigPage/PluginConfigPage.tsx @@ -1,15 +1,14 @@ -import React, { FC, useCallback, useEffect, useState } from 'react'; - -import { Legend, LoadingPlaceholder } from '@grafana/ui'; -import { useLocation } from 'react-router-dom'; - -// import logo from '../../img/logo.svg'; import PluginState, { PluginConnectedStatusResponse } from '../../plugin_state'; import ConfigurationForm from './parts/ConfigurationForm'; import RemoveCurrentConfigurationButton from './parts/RemoveCurrentConfigurationButton'; import StatusMessageBlock from './parts/StatusMessageBlock'; -import { DataExporterPluginConfigPageProps } from 'types'; +import { DataExporterPluginConfigPageProps } from '../../types'; + +import { Legend, LoadingPlaceholder } from '@grafana/ui'; +import { useLocation } from 'react-router-dom'; + +import React, { FC, useCallback, useEffect, useState } from 'react'; const PLUGIN_CONFIGURED_QUERY_PARAM = 'pluginConfigured'; const PLUGIN_CONFIGURED_QUERY_PARAM_TRUTHY_VALUE = 'true'; @@ -71,24 +70,30 @@ export const PluginConfigPage: FC = ({ const resetQueryParams = useCallback(() => removePluginConfiguredQueryParams(pluginIsEnabled), [pluginIsEnabled]); - const checkConnection = useCallback(async () => { - setCheckingIfPluginIsConnected(true); - setPluginConnectionCheckError(null); - if (!pluginMetaDataExporterApiUrl) { - setCheckingIfPluginIsConnected(false); - return; - } - const pluginConnectionResponse = await PluginState.checkIfPluginIsConnected(pluginMetaDataExporterApiUrl); + const checkConnection = useCallback( + async (grafanaDataExporterUrl?: string) => { + setCheckingIfPluginIsConnected(true); + setPluginConnectionCheckError(null); - if (typeof pluginConnectionResponse === 'string') { - setPluginConnectionCheckError(pluginConnectionResponse); - } else { - setPluginIsConnected(pluginConnectionResponse); - reloadPageWithPluginConfiguredQueryParams(pluginConnectionResponse, true); - } + const backendUrl = grafanaDataExporterUrl || pluginMetaDataExporterApiUrl; - setCheckingIfPluginIsConnected(false); - }, [pluginMetaDataExporterApiUrl]); + if (!backendUrl) { + setCheckingIfPluginIsConnected(false); + return; + } + const pluginConnectionResponse = await PluginState.checkIfPluginIsConnected(backendUrl); + + if (typeof pluginConnectionResponse === 'string') { + setPluginConnectionCheckError(pluginConnectionResponse); + } else { + setPluginIsConnected(pluginConnectionResponse); + reloadPageWithPluginConfiguredQueryParams(pluginConnectionResponse, true); + } + + setCheckingIfPluginIsConnected(false); + }, + [pluginMetaDataExporterApiUrl] + ); useEffect(resetQueryParams, [resetQueryParams]); @@ -166,10 +171,7 @@ export const PluginConfigPage: FC = ({ Configure DataExporter {pluginIsConnected ? ( <> -

- Plugin is connected! Continue to DataExporter by clicking the{' '} - {/* DataExporter Logo icon over there 👈 */} -

+

Plugin is connected! You can now go to a dashboard and add the DataExporter panel there.

) : ( diff --git a/src/components/PluginConfigPage/parts/ConfigurationForm/index.tsx b/src/components/PluginConfigPage/parts/ConfigurationForm/index.tsx index 7ed24e4..7e499d2 100644 --- a/src/components/PluginConfigPage/parts/ConfigurationForm/index.tsx +++ b/src/components/PluginConfigPage/parts/ConfigurationForm/index.tsx @@ -16,7 +16,7 @@ import { isEmpty } from 'lodash-es'; const cx = cn.bind(styles); type Props = { - onSuccessfulSetup: () => void; + onSuccessfulSetup: (grafanaDataExporterUrl: string) => void; defaultDataExporterApiUrl: string; }; @@ -68,7 +68,7 @@ const ConfigurationForm: FC = ({ onSuccessfulSetup, defaultDataExporterAp const errorMsg = await PluginState.installPlugin(dataExporterApiUrl); if (!errorMsg) { - onSuccessfulSetup(); + onSuccessfulSetup(dataExporterApiUrl); } else { setSetupErrorMsg(errorMsg); setFormLoading(false); diff --git a/src/plugin_state.ts b/src/plugin_state.ts index 3aa8344..eccf9d5 100644 --- a/src/plugin_state.ts +++ b/src/plugin_state.ts @@ -14,10 +14,7 @@ export type UpdateGrafanaPluginSettingsProps = { secureJsonData?: Partial; }; -type InstallPluginResponse = Pick< - DataExporterPluginMetaSecureJSONData, - 'grafanaToken' -> & { +type InstallPluginResponse = Pick & { dataExporterAPIResponse: DataExporterAPIResponse; }; @@ -125,13 +122,14 @@ class PluginState { static timeout = (pollCount: number) => new Promise((resolve) => setTimeout(resolve, 10 * 2 ** pollCount)); static connectBackend = async (): Promise> => { - // TODO: try to disable success alerts from Grafana API - const { key: grafanaToken } = await this.createGrafanaToken(); - await this.updateGrafanaPluginSettings({ secureJsonData: { grafanaToken } }); + const { key: apiToken } = await this.createGrafanaToken(); + await this.updateGrafanaPluginSettings({ secureJsonData: { apiToken } }); + // TODO: display alert on error const dataExporterAPIResponse = await queryApi(`/connect`, { method: 'POST', + data: { apiToken, url: window.location.toString() }, }); - return { grafanaToken, dataExporterAPIResponse }; + return { apiToken, dataExporterAPIResponse }; }; static installPlugin = async (dataExporterApiUrl: string): Promise => { @@ -158,14 +156,14 @@ class PluginState { // Step 3. reprovision the Grafana plugin settings, storing information that we get back from DataExporter's backend try { - const { grafanaToken } = pluginInstallationDataExporterResponse; + const { apiToken } = pluginInstallationDataExporterResponse; await this.updateGrafanaPluginSettings({ jsonData: { dataExporterApiUrl, }, secureJsonData: { - grafanaToken, + apiToken, }, }); } catch (e) { @@ -181,6 +179,7 @@ class PluginState { try { const resp = await queryApi(`/status`, { method: 'GET', + params: { url: window.location.toString() }, }); // TODO: check if the server version is compatible with the plugin @@ -204,7 +203,7 @@ class PluginState { dataExporterApiUrl: null, }; const secureJsonData: Required = { - grafanaToken: null, + apiToken: null, }; return this.updateGrafanaPluginSettings({ jsonData, secureJsonData }, false); diff --git a/src/types.ts b/src/types.ts index a648e0c..126f81c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,7 +5,7 @@ export type DataExporterPluginMetaJSONData = { }; export type DataExporterPluginMetaSecureJSONData = { - grafanaToken: string | null; + apiToken: string | null; }; export type AppRootProps = BaseAppRootProps; From daf409c897c0465aeb9f749516b5c4bbc4052879 Mon Sep 17 00:00:00 2001 From: vargburz Date: Thu, 19 Jan 2023 23:58:32 +0300 Subject: [PATCH 02/13] Panel UI/UX fixes --- .../components/Panel.tsx | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index cb45844..fea7ce7 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -59,6 +59,8 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { const [selectedTimeRange, setTimeRange] = useState(timeRange); const [panelStatus, setPanelStatus] = useState(PanelStatus.LOADING); + const [isStatusError, setStatusError] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); const timeZoneName = convertTimeZoneTypeToName(timeZone); @@ -121,6 +123,16 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { setTasksDataFrame(dataFrame); }, [tasks]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { + if (isStatusError) { + return; + } + setPanelStatus(panelStatus); + if (panelStatus === PanelStatus.DATASOURCE_ERROR || panelStatus === PanelStatus.PERMISSION_ERROR) { + setStatusError(true); + } + }, [panelStatus]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(refresh, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { @@ -145,6 +157,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { .catch((err) => { setPanelStatus(PanelStatus.DATASOURCE_ERROR); console.error('some error', err); + setErrorMessage(`${err.name}: ${err.message}`); }); } @@ -271,48 +284,59 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { } function getDataFrameForTaskTable(tasks: ExportTask[]): DataFrame { + const sortedTasks = _.orderBy(tasks, (task) => task.progress?.time, 'desc'); const dataFrame = toDataFrame({ name: 'A', fields: [ { name: 'Time', type: FieldType.number, - values: _.map(tasks, (task) => convertTimestampToDate(task.progress?.time)), + 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(tasks, (task) => task.username), + values: _.map(sortedTasks, (task) => task.username), }, { name: 'Datasource', type: FieldType.string, - values: _.map(tasks, (task) => task.queries.map((query) => query.datasource?.name).join(',')), + values: _.map(sortedTasks, (task) => task.queries.map((query) => query.datasource?.name).join(',')), }, { name: 'Exported Rows', type: FieldType.number, - values: _.map(tasks, (task) => task.progress?.exportedRowsCount), + values: _.map(sortedTasks, (task) => task.progress?.exportedRowsCount), }, { name: 'Progress', type: FieldType.string, - values: _.map(tasks, (task) => `${((task.progress?.progress || 0) * 100).toFixed(0)}%`), + values: _.map(sortedTasks, (task) => `${((task.progress?.progress || 0) * 100).toFixed(0)}%`), }, { name: 'Status', type: FieldType.string, - values: _.map(tasks, (task) => task.progress?.status), + values: _.map(sortedTasks, (task) => task.progress?.status), }, { name: 'Error', type: FieldType.string, - values: _.map(tasks, (task) => task.progress?.errorMessage || '-'), + values: _.map(sortedTasks, (task) => task.progress?.errorMessage || '-'), }, { name: 'Download CSV', type: FieldType.string, - values: _.map(tasks, () => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`), + values: _.map(sortedTasks, () => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`), config: { custom: { filterable: false, @@ -396,9 +420,13 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {

Datasource is unavailable.

- Click here to configure DataExporter. + If you have not setup the plugin click + + here + + to configure DataExporter.
- {/* TODO: display error message? */} +

{errorMessage}

); const permissionErrorDiv = ( @@ -422,8 +450,9 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { {queriesDataFrame === null ? ( - // TODO: if datasource responds with error, display the error - +
+

There are no queries to export.

+
) : (
@@ -480,4 +509,11 @@ const getStyles = () => ({ z-index: 1061; } `, + customLink: css` + color: #6e9fff; + margin: 0 4px; + &:hover { + text-decoration: underline; + } + `, }); From 45c2e23dbc4da8ad10ba9b8cf792be2baf5068e6 Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 20 Jan 2023 00:00:26 +0300 Subject: [PATCH 03/13] hotfix --- src/plugin_state.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugin_state.ts b/src/plugin_state.ts index eccf9d5..0d7fae6 100644 --- a/src/plugin_state.ts +++ b/src/plugin_state.ts @@ -177,12 +177,13 @@ class PluginState { dataExporterApiUrl: string ): Promise => { try { - const resp = await queryApi(`/status`, { + const resp = await queryApi(`/connect`, { method: 'GET', params: { url: window.location.toString() }, }); // TODO: check if the server version is compatible with the plugin + // TODO: remove configuration if backend says that api key doesn't work if (resp.version) { return resp; } else { From 308f6386abbff739f4b04b7b3423459985147121 Mon Sep 17 00:00:00 2001 From: vargburz Date: Fri, 20 Jan 2023 13:41:25 +0300 Subject: [PATCH 04/13] fix error status chanhing --- .../components/Panel.tsx | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index fea7ce7..614586f 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -59,14 +59,19 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { const [selectedTimeRange, setTimeRange] = useState(timeRange); const [panelStatus, setPanelStatus] = useState(PanelStatus.LOADING); - const [isStatusError, setStatusError] = useState(false); + function setPanelStatusWithValidate(status: PanelStatus): void { + if (panelStatus === PanelStatus.DATASOURCE_ERROR || panelStatus === PanelStatus.PERMISSION_ERROR) { + return; + } + return setPanelStatus(status); + } + const [errorMessage, setErrorMessage] = useState(null); const timeZoneName = convertTimeZoneTypeToName(timeZone); if (contextSrv.user.orgRole !== OrgRole.Admin) { - // TODO: it shouldn't be overriten - setPanelStatus(PanelStatus.PERMISSION_ERROR); + setPanelStatusWithValidate(PanelStatus.PERMISSION_ERROR); } useEffect(() => { @@ -123,17 +128,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { setTasksDataFrame(dataFrame); }, [tasks]); // eslint-disable-line react-hooks/exhaustive-deps - useEffect(() => { - if (isStatusError) { - return; - } - setPanelStatus(panelStatus); - if (panelStatus === PanelStatus.DATASOURCE_ERROR || panelStatus === PanelStatus.PERMISSION_ERROR) { - setStatusError(true); - } - }, [panelStatus]); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(refresh, []); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(fetchTasks, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (queries === null) { @@ -142,26 +137,26 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { setQueriesDataFrame(getDataFrameForQueriesTable(queries)); }, [queries]); // eslint-disable-line react-hooks/exhaustive-deps - function refresh(): void { + function fetchTasks(): void { getTasks() .then((tasks) => { setTasks(tasks); - setPanelStatus(PanelStatus.OK); + setPanelStatusWithValidate(PanelStatus.OK); for (let task of tasks) { if (task.progress?.status === ExportStatus.EXPORTING) { - setTimeout(refresh, 1000); + setTimeout(fetchTasks, 1000); return; } } }) .catch((err) => { - setPanelStatus(PanelStatus.DATASOURCE_ERROR); + setPanelStatusWithValidate(PanelStatus.DATASOURCE_ERROR); console.error('some error', err); setErrorMessage(`${err.name}: ${err.message}`); }); } - eventBus.subscribe(RefreshEvent, refresh); + eventBus.subscribe(RefreshEvent, fetchTasks); function getDatasourceByUid(uid?: string): DataSourceSettings | undefined { if (_.isNil(uid)) { @@ -204,7 +199,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { }, }); - refresh(); + fetchTasks(); onCloseModal(); unselectAllQueries(); @@ -431,8 +426,8 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { ); const permissionErrorDiv = (
-

Permission Error.

-
DataExporter panel availabel only for Admins
+

Permission Error.

+
DataExporter panel available only for Admins.
); const mainDiv = ( From e8d4f3c8c9d53900763fdb0ce18eb9f8fe48fd1b Mon Sep 17 00:00:00 2001 From: vargburz Date: Fri, 20 Jan 2023 13:43:00 +0300 Subject: [PATCH 05/13] fix --- src/panels/corpglory-dataexporter-panel/components/Panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index 614586f..60ef817 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -60,7 +60,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { const [panelStatus, setPanelStatus] = useState(PanelStatus.LOADING); function setPanelStatusWithValidate(status: PanelStatus): void { - if (panelStatus === PanelStatus.DATASOURCE_ERROR || panelStatus === PanelStatus.PERMISSION_ERROR) { + if (panelStatus === PanelStatus.PERMISSION_ERROR) { return; } return setPanelStatus(status); From 8eeaf714945531b0a2c872dd08c4999c1c07e2bd Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 20 Jan 2023 18:17:57 +0300 Subject: [PATCH 06/13] update queries list on Add Task click --- .../components/Panel.tsx | 48 ++++---- .../corpglory-dataexporter-panel/types.ts | 114 ++++++++++++++++++ 2 files changed, 139 insertions(+), 23 deletions(-) diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index 60ef817..f5e31ce 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, ExportTask, DashboardQuery, DatasourceType, ExportStatus, PanelStatus } from '../types'; +import { PanelOptions, ExportTask, DashboardQuery, DatasourceType, ExportStatus, PanelStatus, Dashboard } from '../types'; import { convertTimestampToDate, convertTimeZoneTypeToName, getDashboardUid } from '../../../utils'; import { CLOSE_ICON_BASE_64, DOWNLOAD_ICON_BASE_64, SELECT_ICON_BASE_64, UNSELECT_ICON_BASE_64 } from '../../../icons'; @@ -45,7 +45,7 @@ interface Props extends PanelProps {} export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { // TODO: Dashboard type - const [dashboard, setDashboard] = useState(null); + const [dashboard, setDashboard] = useState(null); const [datasources, setDatasources] = useState(null); const [tasks, setTasks] = useState(null); @@ -93,10 +93,28 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { }, []); useEffect(() => { + if (tasks === null) { + return; + } + const dataFrame = getDataFrameForTaskTable(tasks); + setTasksDataFrame(dataFrame); + }, [tasks]); // 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(dashboard: Dashboard | null, datasources: DataSourceSettings[] | null): Promise { if (!dashboard || !datasources) { + console.warn(`Can't update queries if there is no dashboard or datasources`); return; } - dashboard.panels.forEach((panel: PanelModel) => { + dashboard.panels?.forEach((panel: PanelModel) => { const queries: DashboardQuery[] = []; // @ts-ignore @@ -118,24 +136,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { 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(fetchTasks, []); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - if (queries === null) { - return; - } - setQueriesDataFrame(getDataFrameForQueriesTable(queries)); - }, [queries]); // eslint-disable-line react-hooks/exhaustive-deps + } function fetchTasks(): void { getTasks() @@ -212,7 +213,8 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { setQueries(queries.map((query: DashboardQuery) => ({ ...query, selected: false }))); } - function openDatasourceModal(): void { + function openDatasourceModal(dashboard: Dashboard | null, datasources: DataSourceSettings[] | null): void { + updateQueries(dashboard, datasources); setTimeRange(timeRange); setModalVisibility(true); } @@ -439,7 +441,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { aria-label="Rich history button" icon="plus" style={{ marginTop: '8px' }} - onClick={openDatasourceModal} + onClick={() => openDatasourceModal(dashboard, datasources)} > Add Task diff --git a/src/panels/corpglory-dataexporter-panel/types.ts b/src/panels/corpglory-dataexporter-panel/types.ts index 1af9a8d..1010848 100644 --- a/src/panels/corpglory-dataexporter-panel/types.ts +++ b/src/panels/corpglory-dataexporter-panel/types.ts @@ -49,3 +49,117 @@ export type DashboardQuery = { panel: PanelModel; datasource: DataSourceSettings; }; + +export interface Dashboard { + /** + * TODO docs + */ + annotations?: { + list: Array; + }; + /** + * Description of dashboard. + */ + description?: string; + /** + * Whether a dashboard is editable or not. + */ + editable: boolean; + /** + * TODO docs + */ + fiscalYearStartMonth?: number; + gnetId?: string; + graphTooltip: any; + /** + * Unique numeric identifier for the dashboard. + * TODO must isolate or remove identifiers local to a Grafana instance...? + */ + id?: number; + /** + * TODO docs + */ + links?: Array; + /** + * TODO docs + */ + liveNow?: boolean; + panels?: Array; + /** + * TODO docs + */ + refresh?: (string | false); + /** + * Version of the JSON schema, incremented each time a Grafana update brings + * changes to said schema. + * TODO this is the existing schema numbering system. It will be replaced by Thema's themaVersion + */ + schemaVersion: number; + /** + * Theme of dashboard. + */ + style: ('light' | 'dark'); + /** + * Tags associated with dashboard. + */ + tags?: Array; + /** + * TODO docs + */ + templating?: { + list: Array; + }; + /** + * Time range for dashboard, e.g. last 6 hours, last 7 days, etc + */ + time?: { + from: string; + to: string; + }; + /** + * TODO docs + * TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes + */ + timepicker?: { + /** + * Whether timepicker is collapsed or not. + */ + collapse: boolean; + /** + * Whether timepicker is enabled or not. + */ + enable: boolean; + /** + * Whether timepicker is visible or not. + */ + hidden: boolean; + /** + * Selectable intervals for auto-refresh. + */ + refresh_intervals: Array; + /** + * TODO docs + */ + time_options: Array; + }; + /** + * Timezone of dashboard, + */ + timezone?: ('browser' | 'utc' | string); + /** + * Title of dashboard. + */ + title?: string; + /** + * Unique dashboard identifier that can be generated by anyone. string (8-40) + */ + uid?: string; + /** + * Version of the dashboard, incremented each time the dashboard is updated. + */ + version?: number; + /** + * TODO docs + */ + weekStart?: string; +} From a4e4dc637f5a4d02bfa69c515355416e4a74e274 Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 20 Jan 2023 18:18:46 +0300 Subject: [PATCH 07/13] fix queries list containing queries only from one panel --- .../corpglory-dataexporter-panel/components/Panel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index f5e31ce..6bcf69f 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -114,8 +114,9 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { console.warn(`Can't update queries if there is no dashboard or datasources`); return; } + + const queries: DashboardQuery[] = []; dashboard.panels?.forEach((panel: PanelModel) => { - const queries: DashboardQuery[] = []; // @ts-ignore if (panel.type === PANEL_ID) { @@ -133,9 +134,8 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { } queries.push({ selected: false, target, panel, datasource }); }); - - setQueries(queries); }); + setQueries(queries); } function fetchTasks(): void { From d0319ab96b434781fb4c12da4582fba21da05571 Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 20 Jan 2023 18:20:59 +0300 Subject: [PATCH 08/13] hotfix --- .../corpglory-dataexporter-panel/components/Panel.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index 6bcf69f..d1896d1 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -109,7 +109,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { setQueriesDataFrame(getDataFrameForQueriesTable(queries)); }, [queries]); // eslint-disable-line react-hooks/exhaustive-deps - async function updateQueries(dashboard: Dashboard | null, datasources: DataSourceSettings[] | null): Promise { + async function updateQueries(): Promise { if (!dashboard || !datasources) { console.warn(`Can't update queries if there is no dashboard or datasources`); return; @@ -213,8 +213,8 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { setQueries(queries.map((query: DashboardQuery) => ({ ...query, selected: false }))); } - function openDatasourceModal(dashboard: Dashboard | null, datasources: DataSourceSettings[] | null): void { - updateQueries(dashboard, datasources); + function openDatasourceModal(): void { + updateQueries(); setTimeRange(timeRange); setModalVisibility(true); } @@ -441,7 +441,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { aria-label="Rich history button" icon="plus" style={{ marginTop: '8px' }} - onClick={() => openDatasourceModal(dashboard, datasources)} + onClick={openDatasourceModal} > Add Task From 731e22cb28a93e61cd7046daf61737442b64b639 Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 20 Jan 2023 18:21:21 +0300 Subject: [PATCH 09/13] lint:fix --- .../components/Panel.tsx | 19 +++++++++--------- .../corpglory-dataexporter-panel/types.ts | 20 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index d1896d1..f305e97 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -1,4 +1,12 @@ -import { PanelOptions, ExportTask, DashboardQuery, DatasourceType, ExportStatus, PanelStatus, Dashboard } from '../types'; +import { + PanelOptions, + ExportTask, + DashboardQuery, + DatasourceType, + ExportStatus, + PanelStatus, + Dashboard, +} from '../types'; import { convertTimestampToDate, convertTimeZoneTypeToName, getDashboardUid } from '../../../utils'; import { CLOSE_ICON_BASE_64, DOWNLOAD_ICON_BASE_64, SELECT_ICON_BASE_64, UNSELECT_ICON_BASE_64 } from '../../../icons'; @@ -117,7 +125,6 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { const queries: DashboardQuery[] = []; dashboard.panels?.forEach((panel: PanelModel) => { - // @ts-ignore if (panel.type === PANEL_ID) { return; @@ -436,13 +443,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
- diff --git a/src/panels/corpglory-dataexporter-panel/types.ts b/src/panels/corpglory-dataexporter-panel/types.ts index 1010848..f369b40 100644 --- a/src/panels/corpglory-dataexporter-panel/types.ts +++ b/src/panels/corpglory-dataexporter-panel/types.ts @@ -55,7 +55,7 @@ export interface Dashboard { * TODO docs */ annotations?: { - list: Array; + list: any[]; }; /** * Description of dashboard. @@ -79,16 +79,16 @@ export interface Dashboard { /** * TODO docs */ - links?: Array; + links?: any[]; /** * TODO docs */ liveNow?: boolean; - panels?: Array; + panels?: PanelModel[]; /** * TODO docs */ - refresh?: (string | false); + refresh?: string | false; /** * Version of the JSON schema, incremented each time a Grafana update brings * changes to said schema. @@ -98,16 +98,16 @@ export interface Dashboard { /** * Theme of dashboard. */ - style: ('light' | 'dark'); + style: 'light' | 'dark'; /** * Tags associated with dashboard. */ - tags?: Array; + tags?: string[]; /** * TODO docs */ templating?: { - list: Array; + list: any[]; }; /** * Time range for dashboard, e.g. last 6 hours, last 7 days, etc @@ -136,16 +136,16 @@ export interface Dashboard { /** * Selectable intervals for auto-refresh. */ - refresh_intervals: Array; + refresh_intervals: string[]; /** * TODO docs */ - time_options: Array; + time_options: string[]; }; /** * Timezone of dashboard, */ - timezone?: ('browser' | 'utc' | string); + timezone?: 'browser' | 'utc' | string; /** * Title of dashboard. */ From 4d68bb29c6d1919fa8eb9d661589f4e8a006f1b6 Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 20 Jan 2023 18:39:25 +0300 Subject: [PATCH 10/13] hotfix --- src/panels/corpglory-dataexporter-panel/components/Panel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index f305e97..9d85bee 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -52,7 +52,6 @@ const APP_ID = 'corpglory-dataexporter-app'; interface Props extends PanelProps {} export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { - // TODO: Dashboard type const [dashboard, setDashboard] = useState(null); const [datasources, setDatasources] = useState(null); From 57b80672fba8a372ed366a7a91a722f0876a3eda Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 20 Jan 2023 18:50:20 +0300 Subject: [PATCH 11/13] bind tasks to a dashboard --- .../corpglory-dataexporter-panel/components/Panel.tsx | 11 ++++++++--- src/panels/corpglory-dataexporter-panel/types.ts | 1 + src/services/api_service.ts | 4 ++-- src/utils/index.ts | 6 ++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index 9d85bee..82488a1 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -8,7 +8,7 @@ import { Dashboard, } from '../types'; -import { convertTimestampToDate, convertTimeZoneTypeToName, getDashboardUid } from '../../../utils'; +import { convertTimestampToDate, convertTimeZoneTypeToName, getCurrentDashboardUid } 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'; @@ -83,7 +83,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { useEffect(() => { async function getCurrentDashboard(): Promise { - const currentDashboardUid = getDashboardUid(window.location.toString()); + const currentDashboardUid = getCurrentDashboardUid(); return getDashboardByUid(currentDashboardUid); } @@ -145,7 +145,9 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { } function fetchTasks(): void { - getTasks() + const dashboardUid = getCurrentDashboardUid(); + + getTasks(dashboardUid) .then((tasks) => { setTasks(tasks); setPanelStatusWithValidate(PanelStatus.OK); @@ -187,7 +189,10 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { 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: { diff --git a/src/panels/corpglory-dataexporter-panel/types.ts b/src/panels/corpglory-dataexporter-panel/types.ts index f369b40..5d10ebc 100644 --- a/src/panels/corpglory-dataexporter-panel/types.ts +++ b/src/panels/corpglory-dataexporter-panel/types.ts @@ -33,6 +33,7 @@ export type ExportProgress = { }; export type ExportTask = { + dashboardUid: string; username: string; queries: DashboardQuery[]; timeRange: { diff --git a/src/services/api_service.ts b/src/services/api_service.ts index d12acd0..ce0ee36 100644 --- a/src/services/api_service.ts +++ b/src/services/api_service.ts @@ -43,8 +43,8 @@ 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(dashboardUid: string): Promise { + return queryApi('/task', { params: { dashboardUid } }); } export async function deleteTask(taskId?: string): Promise { diff --git a/src/utils/index.ts b/src/utils/index.ts index 07acebd..cfa615f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,10 +6,12 @@ export function openNotification(message: React.ReactNode) { appEvents.emit(AppEvents.alertSuccess, [message]); } -export function getDashboardUid(url: string): string { +export function getCurrentDashboardUid(): string { + const url = window.location.toString(); + const matches = new URL(url).pathname.match(/\/d\/([^/]+)/); if (!matches) { - throw new Error(`Couldn't parse uid from ${url}`); + throw new Error(`Can't get current dashboard uid. If it's a new dashboard, please save it first.`); } else { return matches[1]; } From b6041c00f70b3f9d8b24dff9c6021745f1682117 Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 20 Jan 2023 21:13:00 +0300 Subject: [PATCH 12/13] do not display unsupported actions for tasks && display success alert on download start --- src/icons.ts | 4 +- .../components/Panel.tsx | 102 +++++++++++------- 2 files changed, 63 insertions(+), 43 deletions(-) diff --git a/src/icons.ts b/src/icons.ts index af34bb9..72e4d55 100644 --- a/src/icons.ts +++ b/src/icons.ts @@ -1,7 +1,7 @@ export const CLOSE_ICON_BASE_64 = 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACJSURBVHgB7ZPRCcAgDESv0lWc0zhC53OIjmAVUpCSamL76YEoyfFIiAGWfldK6SwnKHyhep9xJ3iPcqgH5RyxV1VlBWYJypXVHMEiCaqBbRhAy3W3B76j954wq6ZSVZsOY+WXt6i9l2ymGTlUq0VpOcIqaQC96Zth01DN1zBBefVI4SNp9Za+6wLcH6DKFrfpxgAAAABJRU5ErkJggg=='; -export const DOWNLOAD_ICON_BASE_64 = - 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADlSURBVHgB3ZPNDYJAEIVnN96lBA7A2RIsATuQiiwBSrAD7MAjCZBACXqFwPomMRtEwMVwgZeQhfn5mNnMEG1CaZoeiqKwTWJ3JkFCiEtVVU+8+r9iJRlKSrk3iqOFtWJglmXHf3yDwCRJbBxxnudh34cRYluMMbKGcgWNCIlnjEuIJ1JK2WzDWeKb7YHjONEsYBf6kTABY+mWuYX+3Xiex9UFU7D3FllfwLqueQvi/h8ZCtBprDLY703T6A0yWj2ArmSoxedQV4jSSz5xj4pmCi0/NKfrwNz5bdtaNENciOu6N1qNXhzZXHMb9Q+nAAAAAElFTkSuQmCC'; +export const OPTIONS_ICON_BASE_64 = + 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAS1BMVEUAAADH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0Nn///+uWGxHAAAAF3RSTlMAABI4MAgNNbzy7JcQG6zvoPxPbavwDHoS9oEAAAABYktHRBibaYUeAAAACXBIWXMAAABgAAAAYADwa0LPAAAAB3RJTUUH5wEUFy4lfOQAfAAAAFdJREFUGNPFjkkKwCAQBEcd912TzP9/GglIQvBuXQoamm6ArbCHv7hAOaS0VkMSBWeAxjofIKacU4TgnTUIhYhqgz5EHVodKutw1o98vvU5dH2Hlpe2cgMuNAUd58WuNgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0wMS0yMFQyMDo0NjozNyswMzowMDMOisYAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMDEtMjBUMjA6NDY6MzcrMDM6MDBCUzJ6AAAAAElFTkSuQmCC'; export const UNSELECT_ICON_BASE_64 = 'PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgcng9IjEuNSIgZmlsbD0iIzExMTIxNiIgc3Ryb2tlPSIjMkQyRTM0Ii8+Cjwvc3ZnPgo='; export const SELECT_ICON_BASE_64 = diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index 82488a1..90514ac 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -9,12 +9,12 @@ import { } from '../types'; import { convertTimestampToDate, convertTimeZoneTypeToName, getCurrentDashboardUid } from '../../../utils'; -import { CLOSE_ICON_BASE_64, DOWNLOAD_ICON_BASE_64, SELECT_ICON_BASE_64, UNSELECT_ICON_BASE_64 } from '../../../icons'; +import { CLOSE_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 { appEvents, contextSrv } from 'grafana/app/core/core'; import { css } from '@emotion/css'; import { @@ -40,6 +40,7 @@ import { DataSourceSettings, TimeRange, OrgRole, + AppEvents, } from '@grafana/data'; import { RefreshEvent } from '@grafana/runtime'; @@ -297,7 +298,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { name: 'A', fields: [ { - name: 'Time', + name: 'Status Updated At', type: FieldType.number, values: _.map(sortedTasks, (task) => convertTimestampToDate(task.progress?.time)), }, @@ -342,41 +343,25 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { values: _.map(sortedTasks, (task) => task.progress?.errorMessage || '-'), }, { - name: 'Download CSV', + name: 'Actions', type: FieldType.string, - values: _.map(sortedTasks, () => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`), + values: _.map(sortedTasks, (task) => { + switch (task.progress?.status) { + case ExportStatus.FINISHED: + return `data:image/png;base64,${OPTIONS_ICON_BASE_64}`; + case ExportStatus.ERROR: + return `data:image/png;base64,${CLOSE_ICON_BASE_64}`; + case ExportStatus.EXPORTING: + return ``; + default: + throw new Error(`Unknown exporting status: ${task.progress?.status}`); + } + }), 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), - }, - ], }, }, ], @@ -391,22 +376,57 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { 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(e: DataLinkClickEvent): Promise { - const rowIndex = e.origin.rowIndex; - - const task = _.find(tasks, (task, idx) => idx === rowIndex); - await deleteTask(task?.id); + async function onDeleteClick(taskToDelete: ExportTask): Promise { + await deleteTask(taskToDelete?.id); - const filteredTasks = _.filter(tasks, (task, idx) => idx !== rowIndex); + const filteredTasks = _.filter(tasks, (task) => task.id !== taskToDelete.id); setTasks(filteredTasks); } - function onDownloadClick(e: DataLinkClickEvent): void { - const rowIndex = e.origin.rowIndex; - const task = _.find(tasks, (task, idx) => idx === rowIndex); + function onDownloadClick(task: ExportTask): void { + appEvents.emit(AppEvents.alertSuccess, ['CSV has started downloading...']); getStaticFile(task?.id); } From 19c2f5a13f0b94b217bddda13d139b23e701b1ae Mon Sep 17 00:00:00 2001 From: rozetko Date: Fri, 20 Jan 2023 21:19:48 +0300 Subject: [PATCH 13/13] 1.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a3e3480..e99bd70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "corpglory-dataexporter-app", - "version": "1.0.0", + "version": "1.0.1", "description": "", "scripts": { "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 ./src",