|
|
|
@ -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<boolean>(false); |
|
|
|
|
|
|
|
|
|
const [csvDelimiter, setCsvDelimiter] = useState<string>(','); |
|
|
|
|
|
|
|
|
|
const [selectedTimeRange, setTimeRange] = useState<TimeRange>(timeRange); |
|
|
|
|
|
|
|
|
|
const [panelStatus, setPanelStatus] = useState<PanelStatus>(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<void> { |
|
|
|
|
async function onDeleteClick(event: DataLinkClickEvent): Promise<void> { |
|
|
|
|
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<void> { |
|
|
|
|
async function onDownloadClick(event: DataLinkClickEvent): Promise<void> { |
|
|
|
|
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 = <LoadingPlaceholder text="Loading..."></LoadingPlaceholder>; |
|
|
|
@ -468,7 +498,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
|
|
|
|
|
const permissionErrorDiv = ( |
|
|
|
|
<div> |
|
|
|
|
<p> Permission Error. </p> |
|
|
|
|
<div> DataExporter panel available only for Admins. </div> |
|
|
|
|
<div> DataExporter panel available only for Admins and Editors. </div> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
const mainDiv = ( |
|
|
|
@ -495,7 +525,17 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
|
|
|
|
|
/> |
|
|
|
|
</HorizontalGroup> |
|
|
|
|
<Table width={width / 2 - 20} height={height - 40} data={queriesDataFrame} /> |
|
|
|
|
<HorizontalGroup justify="flex-end" spacing="md"> |
|
|
|
|
<HorizontalGroup justify="space-between" spacing="md"> |
|
|
|
|
<HorizontalGroup justify="flex-end" spacing="md"> |
|
|
|
|
<span>CSV delimiter:</span> |
|
|
|
|
<Select |
|
|
|
|
options={delimeterOptions} |
|
|
|
|
value={csvDelimiter} |
|
|
|
|
onChange={(el) => { |
|
|
|
|
setCsvDelimiter(el.value); |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
</HorizontalGroup> |
|
|
|
|
<Button |
|
|
|
|
variant="primary" |
|
|
|
|
aria-label="Add task button" |
|
|
|
|