Browse Source

Merge branch 'master' into dist

dist
rozetko 1 year ago
parent
commit
563cb497a4
  1. 2
      package.json
  2. 56
      src/components/PluginConfigPage/PluginConfigPage.tsx
  3. 4
      src/components/PluginConfigPage/parts/ConfigurationForm/index.tsx
  4. 4
      src/icons.ts
  5. 252
      src/panels/corpglory-dataexporter-panel/components/Panel.tsx
  6. 115
      src/panels/corpglory-dataexporter-panel/types.ts
  7. 24
      src/plugin_state.ts
  8. 4
      src/services/api_service.ts
  9. 2
      src/types.ts
  10. 6
      src/utils/index.ts

2
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",

56
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<DataExporterPluginConfigPageProps> = ({
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<DataExporterPluginConfigPageProps> = ({
<Legend>Configure DataExporter</Legend>
{pluginIsConnected ? (
<>
<p>
Plugin is connected! Continue to DataExporter by clicking the{' '}
{/* <img alt="DataExporter Logo" src={logo} width={18} /> icon over there 👈 */}
</p>
<p>Plugin is connected! You can now go to a dashboard and add the DataExporter panel there.</p>
<StatusMessageBlock text={`Connected to DataExporter`} />
</>
) : (

4
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<Props> = ({ onSuccessfulSetup, defaultDataExporterAp
const errorMsg = await PluginState.installPlugin(dataExporterApiUrl);
if (!errorMsg) {
onSuccessfulSetup();
onSuccessfulSetup(dataExporterApiUrl);
} else {
setSetupErrorMsg(errorMsg);
setFormLoading(false);

4
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 =

252
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<PanelOptions> {}
export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
// TODO: Dashboard type
const [dashboard, setDashboard] = useState<any | null>(null);
const [dashboard, setDashboard] = useState<Dashboard | null>(null);
const [datasources, setDatasources] = useState<DataSourceSettings[] | null>(null);
const [tasks, setTasks] = useState<ExportTask[] | null>(null);
@ -59,17 +67,24 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
const [selectedTimeRange, setTimeRange] = useState<TimeRange>(timeRange);
const [panelStatus, setPanelStatus] = useState<PanelStatus>(PanelStatus.LOADING);
function setPanelStatusWithValidate(status: PanelStatus): void {
if (panelStatus === PanelStatus.PERMISSION_ERROR) {
return;
}
return setPanelStatus(status);
}
const [errorMessage, setErrorMessage] = useState<string | null>(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<any> {
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<void> {
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<void> {
const rowIndex = e.origin.rowIndex;
const task = _.find(tasks, (task, idx) => idx === rowIndex);
await deleteTask(task?.id);
async function onDeleteClick(taskToDelete: ExportTask): Promise<void> {
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) {
<div>
<p>Datasource is unavailable.</p>
<div>
Click <a href={`/plugins/${APP_ID}`}>here</a> to configure DataExporter.
If you have not setup the plugin click
<a className={styles.customLink} href={`/plugins/${APP_ID}`}>
here
</a>
to configure DataExporter.
</div>
{/* TODO: display error message? */}
<p style={{ marginTop: '16px' }}>{errorMessage}</p>
</div>
);
const permissionErrorDiv = (
<div>
<p>Permission Error.</p>
<div> DataExporter panel availabel only for Admins </div>
<p> Permission Error. </p>
<div> DataExporter panel available only for Admins. </div>
</div>
);
const mainDiv = (
<div>
<Table width={width} height={height - 40} data={tasksDataFrame as DataFrame} />
<HorizontalGroup justify="flex-end">
<Button
variant="primary"
aria-label="Rich history button"
icon="plus"
style={{ marginTop: '8px' }}
onClick={openDatasourceModal}
>
<Button variant="primary" icon="plus" style={{ marginTop: '8px' }} onClick={openDatasourceModal}>
Add Task
</Button>
<Modal title="Select Queries" isOpen={isModalOpen} onDismiss={onCloseModal} className={styles.calendarModal}>
{queriesDataFrame === null ? (
// TODO: if datasource responds with error, display the error
<LoadingPlaceholder text="Loading..."></LoadingPlaceholder>
<div>
<p>There are no queries to export.</p>
</div>
) : (
<div>
<VerticalGroup spacing="xs">
@ -480,4 +531,11 @@ const getStyles = () => ({
z-index: 1061;
}
`,
customLink: css`
color: #6e9fff;
margin: 0 4px;
&:hover {
text-decoration: underline;
}
`,
});

115
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;
}

24
src/plugin_state.ts

@ -14,10 +14,7 @@ export type UpdateGrafanaPluginSettingsProps = {
secureJsonData?: Partial<DataExporterPluginMetaSecureJSONData>;
};
type InstallPluginResponse<DataExporterAPIResponse = any> = Pick<
DataExporterPluginMetaSecureJSONData,
'grafanaToken'
> & {
type InstallPluginResponse<DataExporterAPIResponse = any> = Pick<DataExporterPluginMetaSecureJSONData, 'apiToken'> & {
dataExporterAPIResponse: DataExporterAPIResponse;
};
@ -125,13 +122,14 @@ class PluginState {
static timeout = (pollCount: number) => new Promise((resolve) => setTimeout(resolve, 10 * 2 ** pollCount));
static connectBackend = async <RT>(): Promise<InstallPluginResponse<RT>> => {
// 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<RT>(`/connect`, {
method: 'POST',
data: { apiToken, url: window.location.toString() },
});
return { grafanaToken, dataExporterAPIResponse };
return { apiToken, dataExporterAPIResponse };
};
static installPlugin = async (dataExporterApiUrl: string): Promise<string | null> => {
@ -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<PluginConnectedStatusResponse | string> => {
try {
const resp = await queryApi<PluginConnectedStatusResponse>(`/status`, {
const resp = await queryApi<PluginConnectedStatusResponse>(`/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<DataExporterPluginMetaSecureJSONData> = {
grafanaToken: null,
apiToken: null,
};
return this.updateGrafanaPluginSettings({ jsonData, secureJsonData }, false);

4
src/services/api_service.ts

@ -43,8 +43,8 @@ export const queryApi = async <RT = any>(path: string, config: RequestConfig) =>
return response.data as RT;
};
export async function getTasks(): Promise<ExportTask[]> {
return queryApi<ExportTask[]>('/task', {});
export async function getTasks(dashboardUid: string): Promise<ExportTask[]> {
return queryApi<ExportTask[]>('/task', { params: { dashboardUid } });
}
export async function deleteTask(taskId?: string): Promise<void> {

2
src/types.ts

@ -5,7 +5,7 @@ export type DataExporterPluginMetaJSONData = {
};
export type DataExporterPluginMetaSecureJSONData = {
grafanaToken: string | null;
apiToken: string | null;
};
export type AppRootProps = BaseAppRootProps<DataExporterPluginMetaJSONData>;

6
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];
}

Loading…
Cancel
Save