|
|
|
@ -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<PanelOptions> {} |
|
|
|
|
|
|
|
|
|
export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { |
|
|
|
|
// TODO: Dashboard type
|
|
|
|
|
const [dashboard, setDashboard] = useState<any | null>(null); |
|
|
|
|
const [dashboard, setDashboard] = useState<Dashboard | null>(null); |
|
|
|
|
const [datasources, setDatasources] = useState<DataSourceSettings[] | null>(null); |
|
|
|
|
|
|
|
|
|
const [tasks, setTasks] = useState<ExportTask[] | null>(null); |
|
|
|
@ -59,17 +67,24 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
|
|
|
|
|
const [selectedTimeRange, setTimeRange] = useState<TimeRange>(timeRange); |
|
|
|
|
|
|
|
|
|
const [panelStatus, setPanelStatus] = useState<PanelStatus>(PanelStatus.LOADING); |
|
|
|
|
function setPanelStatusWithValidate(status: PanelStatus): void { |
|
|
|
|
if (panelStatus === PanelStatus.PERMISSION_ERROR) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
return setPanelStatus(status); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null); |
|
|
|
|
|
|
|
|
|
const timeZoneName = convertTimeZoneTypeToName(timeZone); |
|
|
|
|
|
|
|
|
|
if (contextSrv.user.orgRole !== OrgRole.Admin) { |
|
|
|
|
// TODO: it shouldn't be overriten
|
|
|
|
|
setPanelStatus(PanelStatus.PERMISSION_ERROR); |
|
|
|
|
setPanelStatusWithValidate(PanelStatus.PERMISSION_ERROR); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
async function getCurrentDashboard(): Promise<any> { |
|
|
|
|
const currentDashboardUid = getDashboardUid(window.location.toString()); |
|
|
|
|
const currentDashboardUid = getCurrentDashboardUid(); |
|
|
|
|
|
|
|
|
|
return getDashboardByUid(currentDashboardUid); |
|
|
|
|
} |
|
|
|
@ -86,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<void> { |
|
|
|
|
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; |
|
|
|
@ -108,47 +141,32 @@ 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; |
|
|
|
|
setQueries(queries); |
|
|
|
|
} |
|
|
|
|
const dataFrame = getDataFrameForTaskTable(tasks); |
|
|
|
|
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 fetchTasks(): void { |
|
|
|
|
const dashboardUid = getCurrentDashboardUid(); |
|
|
|
|
|
|
|
|
|
function refresh(): void { |
|
|
|
|
getTasks() |
|
|
|
|
getTasks(dashboardUid) |
|
|
|
|
.then((tasks) => { |
|
|
|
|
setTasks(tasks); |
|
|
|
|
setPanelStatus(PanelStatus.OK); |
|
|
|
|
setPanelStatusWithValidate(PanelStatus.OK); |
|
|
|
|
for (let task of tasks) { |
|
|
|
|
if (task.progress?.status === ExportStatus.EXPORTING) { |
|
|
|
|
setTimeout(refresh, 1000); |
|
|
|
|
setTimeout(fetchTasks, 1000); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
.catch((err) => { |
|
|
|
|
setPanelStatus(PanelStatus.DATASOURCE_ERROR); |
|
|
|
|
setPanelStatusWithValidate(PanelStatus.DATASOURCE_ERROR); |
|
|
|
|
console.error('some error', err); |
|
|
|
|
setErrorMessage(`${err.name}: ${err.message}`); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
eventBus.subscribe(RefreshEvent, refresh); |
|
|
|
|
eventBus.subscribe(RefreshEvent, fetchTasks); |
|
|
|
|
|
|
|
|
|
function getDatasourceByUid(uid?: string): DataSourceSettings | undefined { |
|
|
|
|
if (_.isNil(uid)) { |
|
|
|
@ -172,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: { |
|
|
|
@ -191,7 +212,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
|
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
refresh(); |
|
|
|
|
fetchTasks(); |
|
|
|
|
|
|
|
|
|
onCloseModal(); |
|
|
|
|
unselectAllQueries(); |
|
|
|
@ -205,6 +226,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function openDatasourceModal(): void { |
|
|
|
|
updateQueries(); |
|
|
|
|
setTimeRange(timeRange); |
|
|
|
|
setModalVisibility(true); |
|
|
|
|
} |
|
|
|
@ -271,80 +293,75 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getDataFrameForTaskTable(tasks: ExportTask[]): DataFrame { |
|
|
|
|
const sortedTasks = _.orderBy(tasks, (task) => task.progress?.time, 'desc'); |
|
|
|
|
const dataFrame = toDataFrame({ |
|
|
|
|
name: 'A', |
|
|
|
|
fields: [ |
|
|
|
|
{ |
|
|
|
|
name: 'Time', |
|
|
|
|
name: 'Status Updated At', |
|
|
|
|
type: FieldType.number, |
|
|
|
|
values: _.map(tasks, (task) => convertTimestampToDate(task.progress?.time)), |
|
|
|
|
values: _.map(sortedTasks, (task) => convertTimestampToDate(task.progress?.time)), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'From', |
|
|
|
|
type: FieldType.number, |
|
|
|
|
values: _.map(sortedTasks, (task) => convertTimestampToDate(task.timeRange.from)), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'To', |
|
|
|
|
type: FieldType.number, |
|
|
|
|
values: _.map(sortedTasks, (task) => convertTimestampToDate(task.timeRange.to)), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'User', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(tasks, (task) => task.username), |
|
|
|
|
values: _.map(sortedTasks, (task) => task.username), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Datasource', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(tasks, (task) => task.queries.map((query) => query.datasource?.name).join(',')), |
|
|
|
|
values: _.map(sortedTasks, (task) => task.queries.map((query) => query.datasource?.name).join(',')), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Exported Rows', |
|
|
|
|
type: FieldType.number, |
|
|
|
|
values: _.map(tasks, (task) => task.progress?.exportedRowsCount), |
|
|
|
|
values: _.map(sortedTasks, (task) => task.progress?.exportedRowsCount), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Progress', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(tasks, (task) => `${((task.progress?.progress || 0) * 100).toFixed(0)}%`), |
|
|
|
|
values: _.map(sortedTasks, (task) => `${((task.progress?.progress || 0) * 100).toFixed(0)}%`), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Status', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(tasks, (task) => task.progress?.status), |
|
|
|
|
values: _.map(sortedTasks, (task) => task.progress?.status), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Error', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(tasks, (task) => task.progress?.errorMessage || '-'), |
|
|
|
|
values: _.map(sortedTasks, (task) => task.progress?.errorMessage || '-'), |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: 'Download CSV', |
|
|
|
|
name: 'Actions', |
|
|
|
|
type: FieldType.string, |
|
|
|
|
values: _.map(tasks, () => `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(tasks, () => `data:image/png;base64,${CLOSE_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: 'Delete', |
|
|
|
|
url: '#', |
|
|
|
|
onClick: (event: DataLinkClickEvent) => onDeleteClick(event), |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
], |
|
|
|
@ -359,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<void> { |
|
|
|
|
const rowIndex = e.origin.rowIndex; |
|
|
|
|
|
|
|
|
|
const task = _.find(tasks, (task, idx) => idx === rowIndex); |
|
|
|
|
await deleteTask(task?.id); |
|
|
|
|
async function onDeleteClick(taskToDelete: ExportTask): Promise<void> { |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -396,34 +448,33 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
|
|
|
|
|
<div> |
|
|
|
|
<p>Datasource is unavailable.</p> |
|
|
|
|
<div> |
|
|
|
|
Click <a href={`/plugins/${APP_ID}`}>here</a> to configure DataExporter. |
|
|
|
|
If you have not setup the plugin click |
|
|
|
|
<a className={styles.customLink} href={`/plugins/${APP_ID}`}> |
|
|
|
|
here |
|
|
|
|
</a> |
|
|
|
|
to configure DataExporter. |
|
|
|
|
</div> |
|
|
|
|
{/* TODO: display error message? */} |
|
|
|
|
<p style={{ marginTop: '16px' }}>{errorMessage}</p> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
const permissionErrorDiv = ( |
|
|
|
|
<div> |
|
|
|
|
<p> Permission Error. </p> |
|
|
|
|
<div> DataExporter panel availabel only for Admins </div> |
|
|
|
|
<div> DataExporter panel available 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} |
|
|
|
|
> |
|
|
|
|
<Button variant="primary" icon="plus" style={{ marginTop: '8px' }} onClick={openDatasourceModal}> |
|
|
|
|
Add Task |
|
|
|
|
</Button> |
|
|
|
|
<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> |
|
|
|
|
<div> |
|
|
|
|
<p>There are no queries to export.</p> |
|
|
|
|
</div> |
|
|
|
|
) : ( |
|
|
|
|
<div> |
|
|
|
|
<VerticalGroup spacing="xs"> |
|
|
|
@ -480,4 +531,11 @@ const getStyles = () => ({
|
|
|
|
|
z-index: 1061; |
|
|
|
|
} |
|
|
|
|
`,
|
|
|
|
|
customLink: css` |
|
|
|
|
color: #6e9fff; |
|
|
|
|
margin: 0 4px; |
|
|
|
|
&:hover { |
|
|
|
|
text-decoration: underline; |
|
|
|
|
} |
|
|
|
|
`,
|
|
|
|
|
}); |
|
|
|
|