wrapping-up #13

Merged
rozetko merged 7 commits from wrapping-up into master 2 years ago
  1. 4
      src/icons.ts
  2. 181
      src/panels/corpglory-dataexporter-panel/components/Panel.tsx
  3. 115
      src/panels/corpglory-dataexporter-panel/types.ts
  4. 4
      src/services/api_service.ts
  5. 6
      src/utils/index.ts

4
src/icons.ts

@ -1,7 +1,7 @@
export const CLOSE_ICON_BASE_64 =
'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACJSURBVHgB7ZPRCcAgDESv0lWc0zhC53OIjmAVUpCSamL76YEoyfFIiAGWfldK6SwnKHyhep9xJ3iPcqgH5RyxV1VlBWYJypXVHMEiCaqBbRhAy3W3B76j954wq6ZSVZsOY+WXt6i9l2ymGTlUq0VpOcIqaQC96Zth01DN1zBBefVI4SNp9Za+6wLcH6DKFrfpxgAAAABJRU5ErkJggg==';
export const DOWNLOAD_ICON_BASE_64 =
'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADlSURBVHgB3ZPNDYJAEIVnN96lBA7A2RIsATuQiiwBSrAD7MAjCZBACXqFwPomMRtEwMVwgZeQhfn5mNnMEG1CaZoeiqKwTWJ3JkFCiEtVVU+8+r9iJRlKSrk3iqOFtWJglmXHf3yDwCRJbBxxnudh34cRYluMMbKGcgWNCIlnjEuIJ1JK2WzDWeKb7YHjONEsYBf6kTABY+mWuYX+3Xiex9UFU7D3FllfwLqueQvi/h8ZCtBprDLY703T6A0yWj2ArmSoxedQV4jSSz5xj4pmCi0/NKfrwNz5bdtaNENciOu6N1qNXhzZXHMb9Q+nAAAAAElFTkSuQmCC';
export const OPTIONS_ICON_BASE_64 =
'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAS1BMVEUAAADH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0NnH0Nn///+uWGxHAAAAF3RSTlMAABI4MAgNNbzy7JcQG6zvoPxPbavwDHoS9oEAAAABYktHRBibaYUeAAAACXBIWXMAAABgAAAAYADwa0LPAAAAB3RJTUUH5wEUFy4lfOQAfAAAAFdJREFUGNPFjkkKwCAQBEcd912TzP9/GglIQvBuXQoamm6ArbCHv7hAOaS0VkMSBWeAxjofIKacU4TgnTUIhYhqgz5EHVodKutw1o98vvU5dH2Hlpe2cgMuNAUd58WuNgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0wMS0yMFQyMDo0NjozNyswMzowMDMOisYAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMDEtMjBUMjA6NDY6MzcrMDM6MDBCUzJ6AAAAAElFTkSuQmCC';
export const UNSELECT_ICON_BASE_64 =
'PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgcng9IjEuNSIgZmlsbD0iIzExMTIxNiIgc3Ryb2tlPSIjMkQyRTM0Ii8+Cjwvc3ZnPgo=';
export const SELECT_ICON_BASE_64 =

181
src/panels/corpglory-dataexporter-panel/components/Panel.tsx

@ -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);
@ -76,7 +84,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
useEffect(() => {
async function getCurrentDashboard(): Promise<any> {
const currentDashboardUid = getDashboardUid(window.location.toString());
const currentDashboardUid = getCurrentDashboardUid();
return getDashboardByUid(currentDashboardUid);
}
@ -93,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;
@ -115,30 +141,14 @@ 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;
}
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
setQueries(queries);
}
function fetchTasks(): void {
getTasks()
const dashboardUid = getCurrentDashboardUid();
getTasks(dashboardUid)
.then((tasks) => {
setTasks(tasks);
setPanelStatusWithValidate(PanelStatus.OK);
@ -180,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: {
@ -213,6 +226,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
}
function openDatasourceModal(): void {
updateQueries();
setTimeRange(timeRange);
setModalVisibility(true);
}
@ -284,7 +298,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
name: 'A',
fields: [
{
name: 'Time',
name: 'Status Updated At',
type: FieldType.number,
values: _.map(sortedTasks, (task) => convertTimestampToDate(task.progress?.time)),
},
@ -329,41 +343,25 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
values: _.map(sortedTasks, (task) => task.progress?.errorMessage || '-'),
},
{
name: 'Download CSV',
name: 'Actions',
type: FieldType.string,
values: _.map(sortedTasks, () => `data:image/png;base64,${DOWNLOAD_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: 'Download',
url: '#',
onClick: (event: DataLinkClickEvent) => onDownloadClick(event),
},
],
},
},
{
name: 'Delete task',
type: FieldType.string,
values: _.map(tasks, () => `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),
},
],
},
},
],
@ -378,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);
}
@ -434,13 +467,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
<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}>

115
src/panels/corpglory-dataexporter-panel/types.ts

@ -33,6 +33,7 @@ export type ExportProgress = {
};
export type ExportTask = {
dashboardUid: string;
username: string;
queries: DashboardQuery[];
timeRange: {
@ -49,3 +50,117 @@ export type DashboardQuery = {
panel: PanelModel;
datasource: DataSourceSettings;
};
export interface Dashboard {
/**
* TODO docs
*/
annotations?: {
list: any[];
};
/**
* Description of dashboard.
*/
description?: string;
/**
* Whether a dashboard is editable or not.
*/
editable: boolean;
/**
* TODO docs
*/
fiscalYearStartMonth?: number;
gnetId?: string;
graphTooltip: any;
/**
* Unique numeric identifier for the dashboard.
* TODO must isolate or remove identifiers local to a Grafana instance...?
*/
id?: number;
/**
* TODO docs
*/
links?: any[];
/**
* TODO docs
*/
liveNow?: boolean;
panels?: PanelModel[];
/**
* TODO docs
*/
refresh?: string | false;
/**
* Version of the JSON schema, incremented each time a Grafana update brings
* changes to said schema.
* TODO this is the existing schema numbering system. It will be replaced by Thema's themaVersion
*/
schemaVersion: number;
/**
* Theme of dashboard.
*/
style: 'light' | 'dark';
/**
* Tags associated with dashboard.
*/
tags?: string[];
/**
* TODO docs
*/
templating?: {
list: any[];
};
/**
* Time range for dashboard, e.g. last 6 hours, last 7 days, etc
*/
time?: {
from: string;
to: string;
};
/**
* TODO docs
* TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes
*/
timepicker?: {
/**
* Whether timepicker is collapsed or not.
*/
collapse: boolean;
/**
* Whether timepicker is enabled or not.
*/
enable: boolean;
/**
* Whether timepicker is visible or not.
*/
hidden: boolean;
/**
* Selectable intervals for auto-refresh.
*/
refresh_intervals: string[];
/**
* TODO docs
*/
time_options: string[];
};
/**
* Timezone of dashboard,
*/
timezone?: 'browser' | 'utc' | string;
/**
* Title of dashboard.
*/
title?: string;
/**
* Unique dashboard identifier that can be generated by anyone. string (8-40)
*/
uid?: string;
/**
* Version of the dashboard, incremented each time the dashboard is updated.
*/
version?: number;
/**
* TODO docs
*/
weekStart?: string;
}

4
src/services/api_service.ts

@ -43,8 +43,8 @@ export const queryApi = async <RT = any>(path: string, config: RequestConfig) =>
return response.data as RT;
};
export async function getTasks(): Promise<ExportTask[]> {
return queryApi<ExportTask[]>('/task', {});
export async function getTasks(dashboardUid: string): Promise<ExportTask[]> {
return queryApi<ExportTask[]>('/task', { params: { dashboardUid } });
}
export async function deleteTask(taskId?: string): Promise<void> {

6
src/utils/index.ts

@ -6,10 +6,12 @@ export function openNotification(message: React.ReactNode) {
appEvents.emit(AppEvents.alertSuccess, [message]);
}
export function getDashboardUid(url: string): string {
export function getCurrentDashboardUid(): string {
const url = window.location.toString();
const matches = new URL(url).pathname.match(/\/d\/([^/]+)/);
if (!matches) {
throw new Error(`Couldn't parse uid from ${url}`);
throw new Error(`Can't get current dashboard uid. If it's a new dashboard, please save it first.`);
} else {
return matches[1];
}

Loading…
Cancel
Save