diff --git a/src/icons.ts b/src/icons.ts index 31cc44b..ce70f06 100644 --- a/src/icons.ts +++ b/src/icons.ts @@ -6,5 +6,9 @@ export const UNSELECT_ICON_BASE_64 = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgcng9IjEuNSIgZmlsbD0iIzExMTIxNiIgc3Ryb2tlPSIjMkQyRTM0Ii8+Cjwvc3ZnPgo='; export const SELECT_ICON_BASE_64 = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiByeD0iMiIgZmlsbD0iIzRBNzFEMiIvPgo8cGF0aCBkPSJNNS45ODI2NCAxMi41MjE3QzUuNzczOTUgMTIuNTIxNyA1LjU4MjY0IDEyLjQ1MjIgNS40MjYxMiAxMi4yOTU2TDEuOTY1MjUgOC44MzQ3NkMxLjY1MjIxIDguNTIxNzIgMS42NTIyMSA4LjAzNDc2IDEuOTY1MjUgNy43MjE3MkMyLjI3ODI5IDcuNDA4NjcgMi43NjUyNSA3LjQwODY3IDMuMDc4MjkgNy43MjE3Mkw2LjAwMDAzIDEwLjYyNjFMMTIuOTM5MiAzLjcwNDMzQzEzLjI1MjIgMy4zOTEyOCAxMy43MzkyIDMuMzkxMjggMTQuMDUyMiAzLjcwNDMzQzE0LjM2NTMgNC4wMTczNyAxNC4zNjUzIDQuNTA0MzMgMTQuMDUyMiA0LjgxNzM3TDYuNTU2NTYgMTIuMjk1NkM2LjM4MjY0IDEyLjQ1MjIgNi4xOTEzNCAxMi41MjE3IDUuOTgyNjQgMTIuNTIxN1YxMi41MjE3WiIgZmlsbD0iI0ZFRkZGRiIvPgo8L3N2Zz4K'; -export const DOWNLOADING_ICON_BASE_64 = +export const PRELOADER_ICON_BASE_64 = 'data:image/gif;base64,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'; +export const DOWNLOAD_ICON_BASE_64 = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADlSURBVHgB3ZPNDYJAEIVnN96lBA7A2RIsATuQiiwBSrAD7MAjCZBACXqFwPomMRtEwMVwgZeQhfn5mNnMEG1CaZoeiqKwTWJ3JkFCiEtVVU+8+r9iJRlKSrk3iqOFtWJglmXHf3yDwCRJbBxxnudh34cRYluMMbKGcgWNCIlnjEuIJ1JK2WzDWeKb7YHjONEsYBf6kTABY+mWuYX+3Xiex9UFU7D3FllfwLqueQvi/h8ZCtBprDLY703T6A0yWj2ArmSoxedQV4jSSz5xj4pmCi0/NKfrwNz5bdtaNENciOu6N1qNXhzZXHMb9Q+nAAAAAElFTkSuQmCC'; +export const DISABLED_DOWNLOAD_ICON_BASE_64 = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAPNJREFUOE9jZKAyYKSyeQxEGXjr1i0DVlbWD4qKig8IOYAoA4NDI/6DDFq7egVB9QQVgAwaNRBvvOAMw9u3bzuoqqoewBaGyHLopmM18Pr16wo1dY33YTGLHCkwdk9Xu6CiouIHogwEKbp161ZCZXXdfGz+62xvSVRRUVmATQ5vssFmKD7DQBbADbx//77Anz9/DGDhBrMd2VBshkFz0QOY9+EGwjRiyw137twJYGBgEMDmTVCYIltElIH40gn9DASlrYqq2v2EShNs8m0tjYHq6uobUCIFxAGF1b9//wRIMZSRkfEBckQSVdqQZAEpiolRCwC5RpEVeMR8nwAAAABJRU5ErkJggg=='; diff --git a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx index 5021fce..012960f 100644 --- a/src/panels/corpglory-dataexporter-panel/components/Panel.tsx +++ b/src/panels/corpglory-dataexporter-panel/components/Panel.tsx @@ -11,21 +11,23 @@ import { import { convertTimestampToDate, convertTimeZoneTypeToName, getCurrentDashboardUid } from '../../../utils'; import { CLOSE_ICON_BASE_64, - DOWNLOADING_ICON_BASE_64, - OPTIONS_ICON_BASE_64, + PRELOADER_ICON_BASE_64, SELECT_ICON_BASE_64, UNSELECT_ICON_BASE_64, + DOWNLOAD_ICON_BASE_64, + DISABLED_DOWNLOAD_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 { Table, Button, + Select, HorizontalGroup, VerticalGroup, Modal, @@ -46,6 +48,8 @@ import { DataSourceSettings, TimeRange, OrgRole, + SelectableValue, + AppEvents, } from '@grafana/data'; import { RefreshEvent } from '@grafana/runtime'; @@ -70,6 +74,8 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { const [isModalOpen, setModalVisibility] = useState(false); + const [csvDelimiter, setCsvDelimiter] = useState(','); + const [selectedTimeRange, setTimeRange] = useState(timeRange); const [panelStatus, setPanelStatus] = useState(PanelStatus.LOADING); @@ -84,7 +90,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { const timeZoneName = convertTimeZoneTypeToName(timeZone); - if (contextSrv.user.orgRole !== OrgRole.Admin) { + if (contextSrv.user.orgRole === OrgRole.Viewer) { setPanelStatusWithValidate(PanelStatus.PERMISSION_ERROR); } @@ -207,6 +213,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { to: timerange[1] * 1000, }, queries: selectedQueries, + csvDelimiter, }; // TODO: move this function to API Service await queryApi('/task', { @@ -300,6 +307,40 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { const dataFrame = toDataFrame({ name: 'A', fields: [ + { + name: 'Download', + type: FieldType.string, + values: _.map(sortedTasks, (task) => { + switch (task.progress?.status) { + case ExportStatus.FINISHED: + if (task.id === taskIdBeingDownloaded) { + return PRELOADER_ICON_BASE_64; + } else { + return DOWNLOAD_ICON_BASE_64; + } + case ExportStatus.ERROR: + return DISABLED_DOWNLOAD_ICON_BASE_64; + case ExportStatus.EXPORTING: + return PRELOADER_ICON_BASE_64; + 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: 'Status Updated At', type: FieldType.number, @@ -320,11 +361,6 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { type: FieldType.string, values: _.map(sortedTasks, (task) => task.username), }, - { - name: 'Datasource', - type: FieldType.string, - values: _.map(sortedTasks, (task) => task.queries.map((query) => query.datasource?.name).join(',')), - }, { name: 'Exported Rows', type: FieldType.number, @@ -346,20 +382,20 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { values: _.map(sortedTasks, (task) => task.progress?.errorMessage || '-'), }, { - name: 'Actions', + name: 'Delete', type: FieldType.string, values: _.map(sortedTasks, (task) => { switch (task.progress?.status) { case ExportStatus.FINISHED: if (task.id === taskIdBeingDownloaded) { - return DOWNLOADING_ICON_BASE_64; + return PRELOADER_ICON_BASE_64; } else { - return OPTIONS_ICON_BASE_64; + return CLOSE_ICON_BASE_64; } case ExportStatus.ERROR: return CLOSE_ICON_BASE_64; case ExportStatus.EXPORTING: - return ''; + return PRELOADER_ICON_BASE_64; default: throw new Error(`Unknown exporting status: ${task.progress?.status}`); } @@ -369,6 +405,14 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { filterable: false, displayMode: 'image', }, + links: [ + { + targetBlank: false, + title: 'Download', + url: '#', + onClick: (event: DataLinkClickEvent) => onDeleteClick(event), + }, + ], }, }, ], @@ -384,55 +428,35 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { replaceVariables: (value: string) => value, }); - // @ts-expect-error - dataFrames[0].fields[9].getLinks = (cell: { valueRowIndex: number }) => { - const rowIndex = cell.valueRowIndex; - const task = _.find(sortedTasks, (task, idx) => idx === rowIndex); - if (!task) { - return; - } - switch (task.progress?.status) { - case ExportStatus.FINISHED: - return [ - { - targetBlank: false, - title: 'Download', - url: '#', - onClick: () => onDownloadClick(task), - }, - { - targetBlank: false, - title: 'Delete', - url: '#', - onClick: () => onDeleteClick(task), - }, - ]; - case ExportStatus.ERROR: - return [ - { - targetBlank: false, - title: 'Delete', - url: '#', - onClick: () => onDeleteClick(task), - }, - ]; - case ExportStatus.EXPORTING: - return []; - default: - throw new Error(`Unknown exporting status: ${task.progress?.status}`); - } - }; return dataFrames[0]; } - async function onDeleteClick(taskToDelete: ExportTask): Promise { + async function onDeleteClick(event: DataLinkClickEvent): Promise { + const rowIndex = event.origin.rowIndex; + const sortedTasks = _.orderBy(tasks, (task) => task.progress?.time, 'desc'); + const taskToDelete = sortedTasks[rowIndex]; + if (taskToDelete.progress?.status === ExportStatus.EXPORTING || taskToDelete.id === taskIdBeingDownloaded) { + appEvents.emit(AppEvents.alertError, ['Data Exporter', 'Active task can`t be deleted']); + return; + } await deleteTask(taskToDelete?.id); const filteredTasks = _.filter(tasks, (task) => task.id !== taskToDelete.id); setTasks(filteredTasks); } - async function onDownloadClick(task: ExportTask): Promise { + async function onDownloadClick(event: DataLinkClickEvent): Promise { + const rowIndex = event.origin.rowIndex; + const sortedTasks = _.orderBy(tasks, (task) => task.progress?.time, 'desc'); + const task = sortedTasks[rowIndex]; + if (task.progress?.status === ExportStatus.EXPORTING || task.id === taskIdBeingDownloaded) { + appEvents.emit(AppEvents.alertError, ['Data Exporter', 'Active task can`t be downloaded']); + return; + } + if (task.progress?.status === ExportStatus.ERROR) { + appEvents.emit(AppEvents.alertError, ['Data Exporter', 'Failed task can`t be downloaded']); + return; + } setTaskIdBeingDownloaded(task.id as string); await getStaticFile(task?.id); setTaskIdBeingDownloaded(null); @@ -448,6 +472,12 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { setQueries(updatedQueries); } + const delimeterOptions: SelectableValue[] = [ + { value: ',', label: 'Comma: ","' }, + { value: ';', label: 'Semicolon: ";"' }, + { value: '\t', label: 'Tabulator: "\\t"' }, + ]; + const styles = useStyles2(getStyles); const loadingDiv = ; @@ -468,7 +498,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { const permissionErrorDiv = (

Permission Error.

-
DataExporter panel available only for Admins.
+
DataExporter panel available only for Admins and Editors.
); const mainDiv = ( @@ -495,7 +525,17 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { /> - + + + CSV delimiter: +