You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
471 lines
14 KiB
471 lines
14 KiB
import { PanelOptions, TaskTableRowConfig, QueryTableRowConfig, DatasourceType, PanelStatus } from '../types'; |
|
|
|
import { convertTimestampToDate, 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'; |
|
import { getDashboardByUid, getDatasources } from '../../../services/grafana_backend_service'; |
|
|
|
import { contextSrv } from 'grafana/app/core/core'; |
|
import { css } from '@emotion/css'; |
|
|
|
import { |
|
Table, |
|
Button, |
|
HorizontalGroup, |
|
VerticalGroup, |
|
Modal, |
|
LoadingPlaceholder, |
|
TimeRangeInput, |
|
useStyles2, |
|
} from '@grafana/ui'; |
|
import { |
|
PanelProps, |
|
toDataFrame, |
|
FieldType, |
|
applyFieldOverrides, |
|
createTheme, |
|
DataFrame, |
|
DataLinkClickEvent, |
|
PanelModel, |
|
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) { |
|
// 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 [tasksDataFrame, setTasksDataFrame] = useState<DataFrame | null>(null); |
|
const [queriesDataFrame, setQueriesDataFrame] = useState<DataFrame | null>(null); |
|
|
|
const [isModalOpen, setModalVisibility] = useState<boolean>(false); |
|
|
|
const [selectedTimeRange, setTimeRange] = useState<TimeRange>(timeRange); |
|
|
|
const [panelStatus, setPanelStatus] = useState<PanelStatus>(PanelStatus.LOADING); |
|
|
|
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()); |
|
|
|
return getDashboardByUid(currentDashboardUid); |
|
} |
|
|
|
getCurrentDashboard() |
|
.then((dash) => setDashboard(dash.dashboard)) |
|
.catch((err) => console.error(err)); |
|
}, []); |
|
|
|
useEffect(() => { |
|
getDatasources() |
|
.then((datasources) => setDatasources(datasources)) |
|
.catch((err) => console.error(err)); |
|
}, []); |
|
|
|
useEffect(() => { |
|
if (!dashboard || !datasources) { |
|
return; |
|
} |
|
dashboard.panels.forEach((panel: PanelModel) => { |
|
const queries: QueryTableRowConfig[] = []; |
|
|
|
// @ts-ignore |
|
if (panel.type === PANEL_ID) { |
|
return; |
|
} |
|
|
|
if (!_.includes(_.values(DatasourceType), panel.datasource?.type)) { |
|
return; |
|
} |
|
|
|
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 }); |
|
}); |
|
|
|
setQueries(queries); |
|
}); |
|
}, [dashboard, datasources]); // eslint-disable-line react-hooks/exhaustive-deps |
|
|
|
useEffect(() => { |
|
// if (tasks === null) { |
|
// return; |
|
// } |
|
const dataFrame = getDataFrameForTaskTable([]); |
|
setTasksDataFrame(dataFrame); |
|
}, [tasks]); // eslint-disable-line react-hooks/exhaustive-deps |
|
|
|
useEffect(refresh, []); // eslint-disable-line react-hooks/exhaustive-deps |
|
|
|
useEffect(() => { |
|
if (queries === null) { |
|
return; |
|
} |
|
setQueriesDataFrame(getDataFrameForQueriesTable(queries)); |
|
}, [queries]); // eslint-disable-line react-hooks/exhaustive-deps |
|
|
|
function refresh(): void { |
|
getTasks() |
|
.then((tasks) => { |
|
setTasks(tasks); |
|
setPanelStatus(PanelStatus.OK); |
|
for (let task of tasks) { |
|
// TODO: ExportStatus enum |
|
if (task.status === 'exporting') { |
|
setTimeout(refresh, 1000); |
|
return; |
|
} |
|
} |
|
}) |
|
.catch((err) => { |
|
setPanelStatus(PanelStatus.DATASOURCE_ERROR); |
|
console.error('some error', err); |
|
}); |
|
} |
|
|
|
eventBus.subscribe(RefreshEvent, refresh); |
|
|
|
function getDatasourceByUid(uid?: string): DataSourceSettings | undefined { |
|
if (_.isNil(uid)) { |
|
console.warn(`uid is required to get datasource`); |
|
return undefined; |
|
} |
|
if (datasources === null) { |
|
console.warn(`there is no datasources yet`); |
|
return undefined; |
|
} |
|
const datasource = _.find(datasources, (datasource: DataSourceSettings) => datasource.uid === uid); |
|
|
|
if (!datasource) { |
|
console.warn(`can't find datasource "${uid}"`); |
|
} |
|
|
|
return datasource; |
|
} |
|
|
|
async function onAddTaskClick(): Promise<void> { |
|
const selectedQueries = _.filter(queries, (query: QueryTableRowConfig) => query.selected); |
|
// TODO: timerange picker |
|
const timerange: [number, number] = [selectedTimeRange.from.unix(), selectedTimeRange.to.unix()]; |
|
// 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, |
|
url: window.location.toString(), |
|
}, |
|
}); |
|
|
|
refresh(); |
|
|
|
onCloseModal(); |
|
unselectAllQueries(); |
|
} |
|
|
|
function unselectAllQueries(): void { |
|
if (queries === null) { |
|
return; |
|
} |
|
setQueries(queries.map((query: QueryTableRowConfig) => ({ ...query, selected: false }))); |
|
} |
|
|
|
function openDatasourceModal(): void { |
|
setTimeRange(timeRange); |
|
setModalVisibility(true); |
|
} |
|
|
|
function onCloseModal(): void { |
|
setModalVisibility(false); |
|
unselectAllQueries(); |
|
} |
|
|
|
function getDataFrameForQueriesTable(configs: QueryTableRowConfig[]): DataFrame { |
|
const dataFrame = toDataFrame({ |
|
name: 'A', |
|
fields: [ |
|
{ |
|
name: 'Select', |
|
type: FieldType.string, |
|
values: _.map( |
|
configs, |
|
(config) => `data:image/svg+xml;base64,${config.selected ? SELECT_ICON_BASE_64 : UNSELECT_ICON_BASE_64}` |
|
), |
|
config: { |
|
custom: { |
|
filterable: false, |
|
displayMode: 'image', |
|
}, |
|
links: [ |
|
{ |
|
targetBlank: false, |
|
title: 'Select', |
|
url: '#', |
|
onClick: (event: DataLinkClickEvent) => onDatasourceSelectClick(event), |
|
}, |
|
], |
|
}, |
|
}, |
|
{ |
|
name: 'Panel', |
|
type: FieldType.string, |
|
values: _.map(configs, (config) => config.panel.title), |
|
}, |
|
{ |
|
name: 'RefId', |
|
type: FieldType.string, |
|
values: _.map(configs, (config) => config.refId), |
|
}, |
|
{ |
|
name: 'Datasource', |
|
type: FieldType.string, |
|
values: _.map(configs, (config) => config.datasource.name), |
|
}, |
|
], |
|
}); |
|
|
|
const dataFrames = applyFieldOverrides({ |
|
data: [dataFrame], |
|
fieldConfig: { |
|
overrides: [], |
|
defaults: {}, |
|
}, |
|
theme: createTheme(), |
|
replaceVariables: (value: string) => value, |
|
}); |
|
return dataFrames[0]; |
|
} |
|
|
|
function getDataFrameForTaskTable(configs: TaskTableRowConfig[]): DataFrame { |
|
const dataFrame = toDataFrame({ |
|
name: 'A', |
|
fields: [ |
|
{ |
|
name: 'Time', |
|
type: FieldType.number, |
|
values: _.map(configs, (config) => convertTimestampToDate(config.timestamp)), |
|
}, |
|
{ |
|
name: 'User', |
|
type: FieldType.string, |
|
values: _.map(configs, (config) => config.username), |
|
}, |
|
{ |
|
name: 'Datasource', |
|
type: FieldType.string, |
|
values: _.map(configs, (config) => getDatasourceByUid(config.datasourceRef?.uid)?.name), |
|
}, |
|
{ |
|
name: 'Exported Rows', |
|
type: FieldType.number, |
|
values: _.map(configs, (config) => config.rowsCount), |
|
}, |
|
{ |
|
name: 'Progress', |
|
type: FieldType.string, |
|
values: _.map(configs, (config) => `${(config.progress * 100).toFixed(0)}%`), |
|
}, |
|
{ |
|
name: 'Status', |
|
type: FieldType.string, |
|
values: _.map(configs, (config) => config.status), |
|
}, |
|
{ |
|
name: 'Download CSV', |
|
type: FieldType.string, |
|
values: _.map(configs, () => `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), |
|
}, |
|
], |
|
}, |
|
}, |
|
{ |
|
name: 'Delete task', |
|
type: FieldType.string, |
|
values: _.map(configs, () => `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), |
|
}, |
|
], |
|
}, |
|
}, |
|
], |
|
}); |
|
|
|
const dataFrames = applyFieldOverrides({ |
|
data: [dataFrame], |
|
fieldConfig: { |
|
overrides: [], |
|
defaults: {}, |
|
}, |
|
theme: createTheme(), |
|
replaceVariables: (value: string) => value, |
|
}); |
|
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?.filename); |
|
|
|
const filteredTasks = _.filter(tasks, (task, idx) => idx !== rowIndex); |
|
setTasks(filteredTasks); |
|
} |
|
|
|
function onDownloadClick(e: DataLinkClickEvent): void { |
|
const rowIndex = e.origin.rowIndex; |
|
const task = _.find(tasks, (task, idx) => idx === rowIndex); |
|
getStaticFile(task?.filename); |
|
} |
|
|
|
function onDatasourceSelectClick(e: DataLinkClickEvent): void { |
|
if (queries === null) { |
|
return; |
|
} |
|
const rowIndex = e.origin.rowIndex; |
|
const updatedQueries = _.clone(queries); |
|
updatedQueries[rowIndex].selected = !updatedQueries[rowIndex].selected; |
|
setQueries(updatedQueries); |
|
} |
|
|
|
const styles = useStyles2(getStyles); |
|
|
|
const loadingDiv = <LoadingPlaceholder text="Loading..."></LoadingPlaceholder>; |
|
// TODO: add styles |
|
const datasourceErrorDiv = ( |
|
<div> |
|
<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> |
|
<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" |
|
aria-label="Rich history button" |
|
icon="plus" |
|
style={{ marginTop: '8px' }} |
|
onClick={openDatasourceModal} |
|
> |
|
Add Task |
|
</Button> |
|
<Modal title="Select Queries" isOpen={isModalOpen} onDismiss={onCloseModal} className={styles.calendarModal}> |
|
{queriesDataFrame === null ? ( |
|
<LoadingPlaceholder text="Loading..."></LoadingPlaceholder> |
|
) : ( |
|
<div> |
|
<VerticalGroup spacing="xs"> |
|
<HorizontalGroup justify="flex-start" spacing="md"> |
|
<TimeRangeInput |
|
value={selectedTimeRange} |
|
onChange={(newTimeRange) => { |
|
setTimeRange(newTimeRange); |
|
}} |
|
/> |
|
</HorizontalGroup> |
|
<Table width={width / 2 - 20} height={height - 40} data={queriesDataFrame} /> |
|
<HorizontalGroup justify="flex-end" spacing="md"> |
|
<Button |
|
variant="primary" |
|
aria-label="Add task button" |
|
onClick={onAddTaskClick} |
|
// TODO: move to function |
|
disabled={!queries?.filter((query: QueryTableRowConfig) => query.selected)?.length} |
|
> |
|
Add Task |
|
</Button> |
|
</HorizontalGroup> |
|
</VerticalGroup> |
|
</div> |
|
)} |
|
</Modal> |
|
</HorizontalGroup> |
|
</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 = () => ({ |
|
calendarModal: css` |
|
section { |
|
position: fixed; |
|
top: 20%; |
|
z-index: 1061; |
|
} |
|
`, |
|
});
|
|
|