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", 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/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 cb45844..90514ac 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -1,12 +1,20 @@ -import { PanelOptions, ExportTask, DashboardQuery, DatasourceType, ExportStatus, PanelStatus } 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'; +import { + PanelOptions, + ExportTask, + DashboardQuery, + DatasourceType, + ExportStatus, + PanelStatus, + Dashboard, +} from '../types'; + +import { convertTimestampToDate, convertTimeZoneTypeToName, getCurrentDashboardUid } from '../../../utils'; +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 { @@ -32,6 +40,7 @@ import { DataSourceSettings, TimeRange, OrgRole, + AppEvents, } from '@grafana/data'; import { RefreshEvent } from '@grafana/runtime'; @@ -44,8 +53,7 @@ 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 [dashboard, setDashboard] = useState(null); const [datasources, setDatasources] = useState(null); const [tasks, setTasks] = useState(null); @@ -59,17 +67,24 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { 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) { - // TODO: it shouldn't be overriten - setPanelStatus(PanelStatus.PERMISSION_ERROR); + setPanelStatusWithValidate(PanelStatus.PERMISSION_ERROR); } useEffect(() => { async function getCurrentDashboard(): Promise { - const currentDashboardUid = getDashboardUid(window.location.toString()); + const currentDashboardUid = getCurrentDashboardUid(); return getDashboardByUid(currentDashboardUid); } @@ -86,12 +101,30 @@ 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(): Promise { if (!dashboard || !datasources) { + console.warn(`Can't update queries if there is no dashboard or datasources`); return; } - dashboard.panels.forEach((panel: PanelModel) => { - const queries: DashboardQuery[] = []; + const queries: DashboardQuery[] = []; + dashboard.panels?.forEach((panel: PanelModel) => { // @ts-ignore if (panel.type === PANEL_ID) { return; @@ -108,47 +141,32 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { } queries.push({ selected: false, target, 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 + setQueries(queries); + } - useEffect(() => { - if (queries === null) { - return; - } - setQueriesDataFrame(getDataFrameForQueriesTable(queries)); - }, [queries]); // eslint-disable-line react-hooks/exhaustive-deps + function fetchTasks(): void { + const dashboardUid = getCurrentDashboardUid(); - function refresh(): void { - getTasks() + getTasks(dashboardUid) .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)) { @@ -172,7 +190,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: { @@ -191,7 +212,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { }, }); - refresh(); + fetchTasks(); onCloseModal(); unselectAllQueries(); @@ -205,6 +226,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { } function openDatasourceModal(): void { + updateQueries(); setTimeRange(timeRange); setModalVisibility(true); } @@ -271,80 +293,75 @@ 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', + name: 'Status Updated At', 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 || '-'), - }, - { - 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), - }, - ], - }, + values: _.map(sortedTasks, (task) => task.progress?.errorMessage || '-'), }, { - name: 'Delete task', + name: 'Actions', type: FieldType.string, - values: _.map(tasks, () => `data:image/png;base64,${CLOSE_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: 'Delete', - url: '#', - onClick: (event: DataLinkClickEvent) => onDeleteClick(event), - }, - ], }, }, ], @@ -359,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); } @@ -396,34 +448,33 @@ 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 = (
-

Permission Error.

-
DataExporter panel availabel only for Admins
+

Permission Error.

+
DataExporter panel available only for Admins.
); const mainDiv = (
- {queriesDataFrame === null ? ( - // TODO: if datasource responds with error, display the error - +
+

There are no queries to export.

+
) : (
@@ -480,4 +531,11 @@ const getStyles = () => ({ z-index: 1061; } `, + customLink: css` + color: #6e9fff; + margin: 0 4px; + &:hover { + text-decoration: underline; + } + `, }); diff --git a/src/panels/corpglory-dataexporter-panel/types.ts b/src/panels/corpglory-dataexporter-panel/types.ts index 1af9a8d..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: { @@ -49,3 +50,117 @@ export type DashboardQuery = { panel: PanelModel; datasource: DataSourceSettings; }; + +export interface Dashboard { + /** + * TODO docs + */ + annotations?: { + list: any[]; + }; + /** + * 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?: any[]; + /** + * TODO docs + */ + liveNow?: boolean; + panels?: PanelModel[]; + /** + * 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?: string[]; + /** + * TODO docs + */ + templating?: { + list: any[]; + }; + /** + * 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: string[]; + /** + * TODO docs + */ + time_options: string[]; + }; + /** + * 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; +} diff --git a/src/plugin_state.ts b/src/plugin_state.ts index 3aa8344..0d7fae6 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) { @@ -179,11 +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 { @@ -204,7 +204,7 @@ class PluginState { dataExporterApiUrl: null, }; const secureJsonData: Required = { - grafanaToken: null, + apiToken: null, }; return this.updateGrafanaPluginSettings({ jsonData, secureJsonData }, false); 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/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; 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]; }