Browse Source

Merge pull request 'Error handling' (#8) from error-handling into master

Reviewed-on: #8
pull/10/head
rozetko 2 years ago
parent
commit
119b556242
  1. 73
      src/panels/corpglory-dataexporter-panel/components/Panel.tsx
  2. 49
      src/panels/corpglory-dataexporter-panel/types.ts
  3. 24
      src/services/api_service.ts
  4. 6
      src/utils/index.ts

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

@ -1,4 +1,4 @@
import { PanelOptions, TaskTableRowConfig, QueryTableRowConfig, DatasourceType } from '../types';
import { PanelOptions, ExportTask, DashboardQuery, DatasourceType, ExportStatus } 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';
@ -44,8 +44,8 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
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);
@ -77,7 +77,7 @@ 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
@ -124,8 +124,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
.then((tasks) => {
setTasks(tasks);
for (let task of tasks) {
// TODO: ExportStatus enum
if (task.status === 'exporting') {
if (task.progress?.status === ExportStatus.EXPORTING) {
setTimeout(refresh, 1000);
return;
}
@ -155,18 +154,23 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
}
async function onAddTaskClick(): Promise<void> {
const selectedQueries = _.filter(queries, (query: QueryTableRowConfig) => query.selected);
// TODO: timerange picker
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(),
},
});
@ -181,7 +185,7 @@ 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 {
@ -193,7 +197,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 +205,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 +226,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.refId),
},
{
name: 'Datasource',
type: FieldType.string,
values: _.map(configs, (config) => config.datasource.name),
values: _.map(queries, (query) => query.datasource.name),
},
],
});
@ -249,44 +253,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 +314,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 +349,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 +358,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 {
@ -409,7 +418,7 @@ export function Panel({ width, height, timeRange, eventBus }: Props) {
aria-label="Add task button"
onClick={onAddTaskClick}
// TODO: move to function
disabled={!queries?.filter((query: QueryTableRowConfig) => query.selected)?.length}
disabled={!queries?.filter((query: DashboardQuery) => query.selected)?.length}
>
Add Task
</Button>

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

@ -1,23 +1,7 @@
import { DataQuery, DataSourceRef, DataSourceSettings, PanelModel } from '@grafana/data';
import { DataQuery, DataSourceSettings, PanelModel } from '@grafana/data';
export interface PanelOptions {}
export type TaskTableRowConfig = {
timestamp: number;
username: string;
datasourceRef: DataSourceRef;
rowsCount: number;
progress: number;
status: string;
filename?: string;
};
export type QueryTableRowConfig = Omit<DataQuery, 'datasource'> & {
selected: boolean;
panel: PanelModel;
datasource: DataSourceSettings;
};
export enum DatasourceType {
INFLUXDB = 'influxdb',
GRAPHITE = 'graphite',
@ -26,3 +10,34 @@ export enum DatasourceType {
ELASTICSEARCH = 'elasticsearch',
MYSQL = 'mysql',
}
export enum ExportStatus {
EXPORTING = 'exporting',
FINISHED = 'finished',
ERROR = 'error',
}
export type ExportProgress = {
time: number;
exportedRowsCount: number;
progress: number;
status: ExportStatus;
errorMessage?: string;
};
export type ExportTask = {
username: string;
queries: DashboardQuery[];
timeRange: {
from: number;
to: number;
};
progress?: ExportProgress;
id?: string;
};
export type DashboardQuery = DataQuery & {
selected: boolean;
panel: PanelModel;
datasource: DataSourceSettings;
};

24
src/services/api_service.ts

@ -1,4 +1,4 @@
import { TaskTableRowConfig } from '../panels/corpglory-dataexporter-panel/types';
import { ExportTask } from '../panels/corpglory-dataexporter-panel/types';
import axios from 'axios';
import * as _ from 'lodash';
@ -43,24 +43,24 @@ export const queryApi = async <RT = any>(path: string, config: RequestConfig) =>
return response.data as RT;
};
export async function getTasks(): Promise<TaskTableRowConfig[]> {
return queryApi<TaskTableRowConfig[]>('/task', {});
export async function getTasks(): Promise<ExportTask[]> {
return queryApi<ExportTask[]>('/task', {});
}
export async function deleteTask(filename?: string): Promise<void> {
if (_.isEmpty(filename)) {
console.warn(`can't delete task without filename`);
export async function deleteTask(taskId?: string): Promise<void> {
if (_.isEmpty(taskId)) {
console.warn(`can't delete task without taskId`);
return;
}
await queryApi<TaskTableRowConfig[]>('/task', { method: 'DELETE', data: { filename } });
await queryApi<ExportTask[]>('/task', { method: 'DELETE', data: { taskId } });
}
export async function getStaticFile(filename?: string): Promise<void> {
if (_.isEmpty(filename)) {
console.warn(`can't download file without name`);
export async function getStaticFile(taskId?: string): Promise<void> {
if (_.isEmpty(taskId)) {
console.warn(`can't download file without taskId`);
return;
}
const respData = await queryApi(`/static/${filename}.csv`, {});
const respData = await queryApi(`/static/${taskId}.csv`, {});
// TODO: check if resp exists
// create file link in browser's memory
const href = URL.createObjectURL(new Blob([respData], { type: 'text/csv' }));
@ -68,7 +68,7 @@ export async function getStaticFile(filename?: string): Promise<void> {
// create "a" HTML element with href to file & click
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}.csv`);
link.setAttribute('download', `${taskId}.csv`);
document.body.appendChild(link);
link.click();

6
src/utils/index.ts

@ -15,7 +15,7 @@ export function getDashboardUid(url: string): string {
}
}
export function convertTimestampToDate(timestamp: number): string {
export function convertTimestampToDate(timestamp?: number): string {
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
@ -24,5 +24,7 @@ export function convertTimestampToDate(timestamp: number): string {
minute: 'numeric',
second: 'numeric',
};
return new Date(timestamp).toLocaleString('en-GB', options);
return timestamp ?
new Date(timestamp).toLocaleString('en-GB', options):
'-';
}

Loading…
Cancel
Save