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 60ef817..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); @@ -76,7 +84,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); } @@ -93,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; @@ -115,30 +141,14 @@ 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(fetchTasks, []); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - if (queries === null) { - return; - } - setQueriesDataFrame(getDataFrameForQueriesTable(queries)); - }, [queries]); // eslint-disable-line react-hooks/exhaustive-deps + setQueries(queries); + } function fetchTasks(): void { - getTasks() + const dashboardUid = getCurrentDashboardUid(); + + getTasks(dashboardUid) .then((tasks) => { setTasks(tasks); setPanelStatusWithValidate(PanelStatus.OK); @@ -180,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: { @@ -213,6 +226,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { } function openDatasourceModal(): void { + updateQueries(); setTimeRange(timeRange); setModalVisibility(true); } @@ -284,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)), }, @@ -329,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), - }, - ], }, }, ], @@ -378,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); } @@ -434,13 +467,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 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/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]; }