|
|
|
@ -1,6 +1,6 @@
|
|
|
|
|
import { PanelOptions, TaskTableRowConfig, QueryTableRowConfig, DatasourceType } from '../types'; |
|
|
|
|
import { PanelOptions, ExportTask, DashboardQuery, DatasourceType, ExportStatus, PanelStatus } from '../types'; |
|
|
|
|
|
|
|
|
|
import { convertTimestampToDate, getDashboardUid } from '../../../utils'; |
|
|
|
|
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 { deleteTask, getStaticFile, getTasks, queryApi } from '../../../services/api_service'; |
|
|
|
@ -31,21 +31,25 @@ import {
|
|
|
|
|
DataQuery, |
|
|
|
|
DataSourceSettings, |
|
|
|
|
TimeRange, |
|
|
|
|
OrgRole, |
|
|
|
|
} from '@grafana/data'; |
|
|
|
|
import { RefreshEvent } from '@grafana/runtime'; |
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from 'react'; |
|
|
|
|
import * as _ from 'lodash'; |
|
|
|
|
|
|
|
|
|
const PANEL_ID = 'corpglory-dataexporter-panel'; |
|
|
|
|
const APP_ID = 'corpglory-dataexporter-app'; |
|
|
|
|
|
|
|
|
|
interface Props extends PanelProps<PanelOptions> {} |
|
|
|
|
|
|
|
|
|
export function Panel({ width, height, timeRange, eventBus }: Props) { |
|
|
|
|
export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { |
|
|
|
|
// TODO: Dashboard type
|
|
|
|
|
const [dashboard, setDashboard] = useState<any | null>(null); |
|
|
|
|
const [datasources, setDatasources] = useState<DataSourceSettings[] | null>(null); |
|
|
|
|
|
|
|
|
|
const [tasks, setTasks] = useState<TaskTableRowConfig[] | null>(null); |
|
|
|
|
const [queries, setQueries] = useState<QueryTableRowConfig[] | null>(null); |
|
|
|
|
const [tasks, setTasks] = useState<ExportTask[] | null>(null); |
|
|
|
|
const [queries, setQueries] = useState<DashboardQuery[] | null>(null); |
|
|
|
|
|
|
|
|
|
const [tasksDataFrame, setTasksDataFrame] = useState<DataFrame | null>(null); |
|
|
|
|
const [queriesDataFrame, setQueriesDataFrame] = useState<DataFrame | null>(null); |
|
|
|
@ -54,6 +58,15 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
|
|
|
|
|
const [selectedTimeRange, setTimeRange] = useState<TimeRange>(timeRange); |
|
|
|
|
|
|
|
|
|
const [panelStatus, setPanelStatus] = useState<PanelStatus>(PanelStatus.LOADING); |
|
|
|
|
|
|
|
|
|
const timeZoneName = convertTimeZoneTypeToName(timeZone); |
|
|
|
|
|
|
|
|
|
if (contextSrv.user.orgRole !== OrgRole.Admin) { |
|
|
|
|
// TODO: it shouldn't be overriten
|
|
|
|
|
setPanelStatus(PanelStatus.PERMISSION_ERROR); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
async function getCurrentDashboard(): Promise<any> { |
|
|
|
|
const currentDashboardUid = getDashboardUid(window.location.toString()); |
|
|
|
@ -77,11 +90,10 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
dashboard.panels.forEach((panel: PanelModel) => { |
|
|
|
|
const queries: QueryTableRowConfig[] = []; |
|
|
|
|
const queries: DashboardQuery[] = []; |
|
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
// TODO: move plugin id to const
|
|
|
|
|
if (panel.type === 'corpglory-dataexporter-panel') { |
|
|
|
|
if (panel.type === PANEL_ID) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -90,12 +102,11 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
panel.targets?.forEach((target: DataQuery) => { |
|
|
|
|
console.log('uid', target.datasource?.uid); |
|
|
|
|
const datasource = getDatasourceByUid(target.datasource?.uid); |
|
|
|
|
if (!datasource) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
queries.push({ ...target, selected: false, panel, datasource }); |
|
|
|
|
queries.push({ selected: false, target, panel, datasource }); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
setQueries(queries); |
|
|
|
@ -123,15 +134,18 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
getTasks() |
|
|
|
|
.then((tasks) => { |
|
|
|
|
setTasks(tasks); |
|
|
|
|
setPanelStatus(PanelStatus.OK); |
|
|
|
|
for (let task of tasks) { |
|
|
|
|
// TODO: ExportStatus enum
|
|
|
|
|
if (task.status === 'exporting') { |
|
|
|
|
if (task.progress?.status === ExportStatus.EXPORTING) { |
|
|
|
|
setTimeout(refresh, 1000); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
.catch((err) => console.error(err)); |
|
|
|
|
.catch((err) => { |
|
|
|
|
setPanelStatus(PanelStatus.DATASOURCE_ERROR); |
|
|
|
|
console.error('some error', err); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
eventBus.subscribe(RefreshEvent, refresh); |
|
|
|
@ -154,20 +168,26 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
return datasource; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function onAddTaskClick(): Promise<void> { |
|
|
|
|
const selectedQueries = _.filter(queries, (query: QueryTableRowConfig) => query.selected); |
|
|
|
|
// TODO: timerange picker
|
|
|
|
|
async function onAddTaskClick(timeZoneName: string): Promise<void> { |
|
|
|
|
const selectedQueries = _.filter(queries, (query: DashboardQuery) => query.selected); |
|
|
|
|
const timerange: [number, number] = [selectedTimeRange.from.unix(), selectedTimeRange.to.unix()]; |
|
|
|
|
|
|
|
|
|
const task: ExportTask = { |
|
|
|
|
// @ts-ignore
|
|
|
|
|
username: contextSrv.user.name, |
|
|
|
|
timeRange: { |
|
|
|
|
from: timerange[0] * 1000, |
|
|
|
|
to: timerange[1] * 1000, |
|
|
|
|
}, |
|
|
|
|
queries: selectedQueries, |
|
|
|
|
}; |
|
|
|
|
// TODO: move this function to API Service
|
|
|
|
|
await queryApi('/task', { |
|
|
|
|
method: 'POST', |
|
|
|
|
data: { |
|
|
|
|
from: timerange[0] * 1000, |
|
|
|
|
to: timerange[1] * 1000, |
|
|
|
|
// @ts-ignore
|
|
|
|
|
username: contextSrv.user.name, |
|
|
|
|
tasks: selectedQueries, |
|
|
|
|
task, |
|
|
|
|
url: window.location.toString(), |
|
|
|
|
timeZoneName, |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
@ -181,10 +201,11 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
if (queries === null) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
setQueries(queries.map((query: QueryTableRowConfig) => ({ ...query, selected: false }))); |
|
|
|
|
setQueries(queries.map((query: DashboardQuery) => ({ ...query, selected: false }))); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function openDatasourceModal(): void { |
|
|
|
|
setTimeRange(timeRange); |
|
|
|
|
setModalVisibility(true); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -193,7 +214,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
unselectAllQueries(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getDataFrameForQueriesTable(configs: QueryTableRowConfig[]): DataFrame { |
|
|
|
|
function getDataFrameForQueriesTable(queries: DashboardQuery[]): DataFrame { |
|
|
|
|
const dataFrame = toDataFrame({ |
|
|
|
|
name: 'A', |
|
|
|
|
fields: [ |
|
|
|
@ -201,8 +222,8 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
name: 'Select', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map( |
|
|
|
|
configs, |
|
|
|
|
(config) => `data:image/svg+xml;base64,${config.selected ? SELECT_ICON_BASE_64 : UNSELECT_ICON_BASE_64}` |
|
|
|
|
queries, |
|
|
|
|
(query) => `data:image/svg+xml;base64,${query.selected ? SELECT_ICON_BASE_64 : UNSELECT_ICON_BASE_64}` |
|
|
|
|
), |
|
|
|
|
config: { |
|
|
|
|
custom: { |
|
|
|
@ -222,17 +243,17 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
{ |
|
|
|
|
name: 'Panel', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(configs, (config) => config.panel.title), |
|
|
|
|
values: _.map(queries, (query) => query.panel.title), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'RefId', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(configs, (config) => config.refId), |
|
|
|
|
values: _.map(queries, (query) => query.target.refId), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Datasource', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(configs, (config) => config.datasource.name), |
|
|
|
|
values: _.map(queries, (query) => query.datasource.name), |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
}); |
|
|
|
@ -249,44 +270,49 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
return dataFrames[0]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getDataFrameForTaskTable(configs: TaskTableRowConfig[]): DataFrame { |
|
|
|
|
function getDataFrameForTaskTable(tasks: ExportTask[]): DataFrame { |
|
|
|
|
const dataFrame = toDataFrame({ |
|
|
|
|
name: 'A', |
|
|
|
|
fields: [ |
|
|
|
|
{ |
|
|
|
|
name: 'Time', |
|
|
|
|
type: FieldType.number, |
|
|
|
|
values: _.map(configs, (config) => convertTimestampToDate(config.timestamp)), |
|
|
|
|
values: _.map(tasks, (task) => convertTimestampToDate(task.progress?.time)), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'User', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(configs, (config) => config.username), |
|
|
|
|
values: _.map(tasks, (task) => task.username), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Datasource', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(configs, (config) => getDatasourceByUid(config.datasourceRef?.uid)?.name), |
|
|
|
|
values: _.map(tasks, (task) => task.queries.map((query) => query.datasource?.name).join(',')), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Exported Rows', |
|
|
|
|
type: FieldType.number, |
|
|
|
|
values: _.map(configs, (config) => config.rowsCount), |
|
|
|
|
values: _.map(tasks, (task) => task.progress?.exportedRowsCount), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Progress', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(configs, (config) => `${(config.progress * 100).toFixed(0)}%`), |
|
|
|
|
values: _.map(tasks, (task) => `${((task.progress?.progress || 0) * 100).toFixed(0)}%`), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Status', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(configs, (config) => config.status), |
|
|
|
|
values: _.map(tasks, (task) => task.progress?.status), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Error', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(tasks, (task) => task.progress?.errorMessage || '-'), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Download CSV', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(configs, () => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`), |
|
|
|
|
values: _.map(tasks, () => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`), |
|
|
|
|
config: { |
|
|
|
|
custom: { |
|
|
|
|
filterable: false, |
|
|
|
@ -305,7 +331,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
{ |
|
|
|
|
name: 'Delete task', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(configs, () => `data:image/png;base64,${CLOSE_ICON_BASE_64}`), |
|
|
|
|
values: _.map(tasks, () => `data:image/png;base64,${CLOSE_ICON_BASE_64}`), |
|
|
|
|
config: { |
|
|
|
|
custom: { |
|
|
|
|
filterable: false, |
|
|
|
@ -340,7 +366,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
const rowIndex = e.origin.rowIndex; |
|
|
|
|
|
|
|
|
|
const task = _.find(tasks, (task, idx) => idx === rowIndex); |
|
|
|
|
await deleteTask(task?.filename); |
|
|
|
|
await deleteTask(task?.id); |
|
|
|
|
|
|
|
|
|
const filteredTasks = _.filter(tasks, (task, idx) => idx !== rowIndex); |
|
|
|
|
setTasks(filteredTasks); |
|
|
|
@ -349,7 +375,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
function onDownloadClick(e: DataLinkClickEvent): void { |
|
|
|
|
const rowIndex = e.origin.rowIndex; |
|
|
|
|
const task = _.find(tasks, (task, idx) => idx === rowIndex); |
|
|
|
|
getStaticFile(task?.filename); |
|
|
|
|
getStaticFile(task?.id); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function onDatasourceSelectClick(e: DataLinkClickEvent): void { |
|
|
|
@ -364,14 +390,26 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
|
|
|
|
|
const styles = useStyles2(getStyles); |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
const loadingDiv = <LoadingPlaceholder text="Loading..."></LoadingPlaceholder>; |
|
|
|
|
// TODO: add styles
|
|
|
|
|
const datasourceErrorDiv = ( |
|
|
|
|
<div> |
|
|
|
|
{tasksDataFrame === null ? ( |
|
|
|
|
// TODO: if datasource responds with error, display the error
|
|
|
|
|
<LoadingPlaceholder text="Loading..."></LoadingPlaceholder> |
|
|
|
|
) : ( |
|
|
|
|
<p>Datasource is unavailable.</p> |
|
|
|
|
<div> |
|
|
|
|
Click <a href={`/plugins/${APP_ID}`}>here</a> to configure DataExporter. |
|
|
|
|
</div> |
|
|
|
|
{/* TODO: display error message? */} |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
const permissionErrorDiv = ( |
|
|
|
|
<div> |
|
|
|
|
<Table width={width} height={height - 40} data={tasksDataFrame} /> |
|
|
|
|
<p>Permission Error.</p> |
|
|
|
|
<div> DataExporter panel availabel 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" |
|
|
|
@ -382,12 +420,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
> |
|
|
|
|
Add Task |
|
|
|
|
</Button> |
|
|
|
|
<Modal |
|
|
|
|
title="Select Queries" |
|
|
|
|
isOpen={isModalOpen} |
|
|
|
|
onDismiss={onCloseModal} |
|
|
|
|
className={styles.calendarModal} |
|
|
|
|
> |
|
|
|
|
<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> |
|
|
|
@ -407,9 +440,9 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
<Button |
|
|
|
|
variant="primary" |
|
|
|
|
aria-label="Add task button" |
|
|
|
|
onClick={onAddTaskClick} |
|
|
|
|
onClick={() => onAddTaskClick(timeZoneName)} |
|
|
|
|
// TODO: move to function
|
|
|
|
|
disabled={!queries?.filter((query: QueryTableRowConfig) => query.selected)?.length} |
|
|
|
|
disabled={!queries?.filter((query: DashboardQuery) => query.selected)?.length} |
|
|
|
|
> |
|
|
|
|
Add Task |
|
|
|
|
</Button> |
|
|
|
@ -420,9 +453,23 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
|
|
|
|
|
</Modal> |
|
|
|
|
</HorizontalGroup> |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
function renderSwitch(panelStatus: PanelStatus): JSX.Element { |
|
|
|
|
switch (panelStatus) { |
|
|
|
|
case PanelStatus.LOADING: |
|
|
|
|
return loadingDiv; |
|
|
|
|
case PanelStatus.DATASOURCE_ERROR: |
|
|
|
|
return datasourceErrorDiv; |
|
|
|
|
case PanelStatus.PERMISSION_ERROR: |
|
|
|
|
return permissionErrorDiv; |
|
|
|
|
case PanelStatus.OK: |
|
|
|
|
return mainDiv; |
|
|
|
|
default: |
|
|
|
|
return datasourceErrorDiv; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return <div>{renderSwitch(panelStatus)}</div>; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const getStyles = () => ({ |
|
|
|
|