Browse Source

Merge pull request 'API connectivity pt.2' (#5) from query-db into master

Reviewed-on: #5
pull/6/head
rozetko 1 year ago
parent
commit
af0b500fb5
  1. 1
      package.json
  2. 8
      src/icons.ts
  3. 596
      src/panels/corpglory-dataexporter-panel/components/Panel.tsx
  4. 28
      src/panels/corpglory-dataexporter-panel/types.ts
  5. 6
      src/plugin_state.ts
  6. 78
      src/services/api_service.ts
  7. 11
      src/services/grafana_backend_service.ts
  8. 41
      src/services/network_service.ts
  9. 21
      src/utils/index.ts
  10. 4
      yarn.lock

1
package.json

@ -49,6 +49,7 @@
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.4.3",
"@types/glob": "^8.0.0",
"@types/grafana": "github:CorpGlory/types-grafana",
"@types/jest": "^27.4.1",
"@types/lodash-es": "^4.17.6",
"@types/node": "^17.0.19",

8
src/icons.ts

@ -0,0 +1,8 @@
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 UNSELECT_ICON_BASE_64 =
'PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgcng9IjEuNSIgZmlsbD0iIzExMTIxNiIgc3Ryb2tlPSIjMkQyRTM0Ii8+Cjwvc3ZnPgo=';
export const SELECT_ICON_BASE_64 =
'PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiByeD0iMiIgZmlsbD0iIzRBNzFEMiIvPgo8cGF0aCBkPSJNNS45ODI2NCAxMi41MjE3QzUuNzczOTUgMTIuNTIxNyA1LjU4MjY0IDEyLjQ1MjIgNS40MjYxMiAxMi4yOTU2TDEuOTY1MjUgOC44MzQ3NkMxLjY1MjIxIDguNTIxNzIgMS42NTIyMSA4LjAzNDc2IDEuOTY1MjUgNy43MjE3MkMyLjI3ODI5IDcuNDA4NjcgMi43NjUyNSA3LjQwODY3IDMuMDc4MjkgNy43MjE3Mkw2LjAwMDAzIDEwLjYyNjFMMTIuOTM5MiAzLjcwNDMzQzEzLjI1MjIgMy4zOTEyOCAxMy43MzkyIDMuMzkxMjggMTQuMDUyMiAzLjcwNDMzQzE0LjM2NTMgNC4wMTczNyAxNC4zNjUzIDQuNTA0MzMgMTQuMDUyMiA0LjgxNzM3TDYuNTU2NTYgMTIuMjk1NkM2LjM4MjY0IDEyLjQ1MjIgNi4xOTEzNCAxMi41MjE3IDUuOTgyNjQgMTIuNTIxN1YxMi41MjE3WiIgZmlsbD0iI0ZFRkZGRiIvPgo8L3N2Zz4K';

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

@ -1,7 +1,12 @@
import { PanelOptions, TaskTableRowConfig, DatasourceTableRowConfig } from '../types';
import { PanelOptions, TaskTableRowConfig, QueryTableRowConfig, DatasourceType } from '../types';
import { getBackendSrv } from '@grafana/runtime';
import { makeRequest } from 'services/network_service';
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 { Table, Button, HorizontalGroup, Modal, LoadingPlaceholder } from '@grafana/ui';
import {
@ -12,91 +17,159 @@ import {
createTheme,
DataFrame,
DataLinkClickEvent,
PanelModel,
DataQuery,
DataSourceSettings,
} from '@grafana/data';
import { RefreshEvent } from '@grafana/runtime';
import React, { useState, useEffect } from 'react';
import * as _ from 'lodash';
interface Props extends PanelProps<PanelOptions> {}
export function Panel({ options, data, width, height, timeRange, onChangeTimeRange }: Props) {
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);
useEffect(() => {
if (tasks === null) {
return;
}
const dataFrame = getDataFrameForTaskTable(tasks, setTasks);
setTasksDataFrame(dataFrame);
}, [tasks]);
const [isModalOpen, setModalVisibility] = useState<boolean>(false);
useEffect(() => {
// TODO: move this function to Network Service
async function getTasks(): Promise<TaskTableRowConfig[]> {
return makeRequest<TaskTableRowConfig[]>('/tasks', {});
async function getCurrentDashboard(): Promise<any> {
const currentDashboardUid = getDashboardUid(window.location.toString());
return getDashboardByUid(currentDashboardUid);
}
getTasks()
.then((tasks) => setTasks(tasks))
getCurrentDashboard()
.then((dash) => setDashboard(dash.dashboard))
.catch((err) => console.error(err));
}, []);
const datasourceConfigs: DatasourceTableRowConfig[] = [];
const [datasources, setDatasources] = useState(datasourceConfigs);
const datasourceDataFrame = getDataFrameForDatasourceTable(datasources, setDatasources);
useEffect(() => {
getDatasources()
.then((datasources) => setDatasources(datasources))
.catch((err) => console.error(err));
}, []);
const [isModalOpen, setModalVisibility] = useState(false);
useEffect(() => {
if (!dashboard || !datasources) {
return;
}
dashboard.panels.forEach((panel: PanelModel) => {
const queries: QueryTableRowConfig[] = [];
const timestampRange: [number, number] = [timeRange.from.unix(), timeRange.to.unix()];
const backendSrv = getBackendSrv();
// @ts-ignore
backendSrv.getInspectorStream().subscribe({
next: (resp: any) => {
const queries = resp?.config?.data?.queries;
if (_.isEmpty(queries)) {
// @ts-ignore
// TODO: move plugin id to const
if (panel.type === 'corpglory-dataexporter-panel') {
return;
}
const datasource = queries[0].datasource.type;
const refId = queries[0].refId;
const rawSql = queries[0].rawSql;
const uid = queries[0].datasource.uid;
// TODO: it works only with sql (rawSql will be empty in prometheus)
const isDatasourceExist = _.some(
datasources,
(ds: DatasourceTableRowConfig) =>
ds.datasource === datasource && ds.refId === refId && _.isEqual(ds.rawSql, rawSql) && ds.uid === uid
);
if (isDatasourceExist) {
if (!_.includes(_.values(DatasourceType), panel.datasource?.type)) {
return;
}
const newConfig = createDatasourceConfig({ datasource, refId, rawSql, uid }, timestampRange);
setDatasources([...datasources, newConfig]);
},
});
function onAddTaskClick(): void {
const selectedDatasources = _.filter(datasources, (datasource: DatasourceTableRowConfig) => datasource.select);
const newTasks = _.map(selectedDatasources, (datasource: DatasourceTableRowConfig) =>
createTaskFromDatasource(datasource)
);
if (_.isEmpty(tasks)) {
setTasks([...newTasks]);
} else {
setTasks([...(tasks as TaskTableRowConfig[]), ...newTasks]);
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(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 refresh(): void {
getTasks()
.then((tasks) => {
setTasks(tasks);
for (let task of tasks) {
// TODO: ExportStatus enum
if (task.status === 'exporting') {
setTimeout(refresh, 1000);
return;
}
}
})
.catch((err) => console.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] = [timeRange.from.unix(), timeRange.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();
unselectAllDatasources();
unselectAllQueries();
}
function unselectAllDatasources(): void {
const updatedDatasources = _.clone(datasources);
for (const ds of updatedDatasources) {
ds.select = false;
function unselectAllQueries(): void {
if (queries === null) {
return;
}
setDatasources(updatedDatasources);
setQueries(queries.map((query: QueryTableRowConfig) => ({ ...query, selected: false })));
}
function openDatasourceModal(): void {
@ -105,7 +178,176 @@ export function Panel({ options, data, width, height, timeRange, onChangeTimeRan
function onCloseModal(): void {
setModalVisibility(false);
unselectAllDatasources();
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);
}
return (
@ -126,18 +368,27 @@ export function Panel({ options, data, width, height, timeRange, onChangeTimeRan
>
Add Task
</Button>
<Modal title="Select Datasources" isOpen={isModalOpen} onDismiss={onCloseModal}>
<Table width={width / 2 - 20} height={height - 40} data={datasourceDataFrame} />
<HorizontalGroup justify="flex-end">
<Button
variant="primary"
aria-label="Add task button"
style={{ marginTop: '8px' }}
onClick={onAddTaskClick}
>
Add Task
</Button>
</HorizontalGroup>
<Modal title="Select Queries" isOpen={isModalOpen} onDismiss={onCloseModal}>
{queriesDataFrame === null ? (
// TODO: if datasource responds with error, display the error
<LoadingPlaceholder text="Loading..."></LoadingPlaceholder>
) : (
<div>
<Table width={width / 2 - 20} height={height - 40} data={queriesDataFrame} />
<HorizontalGroup justify="flex-end">
<Button
variant="primary"
aria-label="Add task button"
style={{ marginTop: '8px' }}
onClick={onAddTaskClick}
// TODO: move to function
disabled={!queries?.filter((query: QueryTableRowConfig) => query.selected)?.length}
>
Add Task
</Button>
</HorizontalGroup>
</div>
)}
</Modal>
</HorizontalGroup>
</div>
@ -145,206 +396,3 @@ export function Panel({ options, data, width, height, timeRange, onChangeTimeRan
</div>
);
}
function getDataFrameForTaskTable(configs: TaskTableRowConfig[], setTasks: any): 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.user),
},
{
name: 'Datasource',
type: FieldType.string,
values: _.map(configs, (config) => config.datasource),
},
{
name: 'Exported Rows',
type: FieldType.number,
values: _.map(configs, (config) => config.rowsCount),
},
{
name: 'Progress',
type: FieldType.string,
values: _.map(configs, (config) => `${config.progress}%`),
},
{
name: 'Status',
type: FieldType.string,
values: _.map(configs, (config) => config.status),
},
{
name: 'Download CSV',
type: FieldType.string,
values: _.map(configs, (config) => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`),
config: {
custom: {
filterable: false,
displayMode: 'image',
},
links: [
{
targetBlank: false,
title: 'CSV link',
url: 'https://chartwerk.io/',
},
],
},
},
{
name: 'Delete task',
type: FieldType.string,
values: _.map(configs, (config) => `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, configs, setTasks),
},
],
},
},
],
});
const dataFrames = applyFieldOverrides({
data: [dataFrame],
fieldConfig: {
overrides: [],
defaults: {},
},
theme: createTheme(),
replaceVariables: (value: string) => value,
});
return dataFrames[0];
}
function getDataFrameForDatasourceTable(configs: DatasourceTableRowConfig[], setDatasources: any): DataFrame {
const dataFrame = toDataFrame({
name: 'A',
fields: [
{
name: 'Select',
type: FieldType.string,
values: _.map(
configs,
(config) => `data:image/svg+xml;base64,${config.select ? 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, configs, setDatasources),
},
],
},
},
{
name: 'Panel',
type: FieldType.string,
values: _.map(configs, (config) => config.panel),
},
{
name: 'RefId',
type: FieldType.string,
values: _.map(configs, (config) => config.refId),
},
{
name: 'Datasource',
type: FieldType.string,
values: _.map(configs, (config) => config.datasource),
},
],
});
const dataFrames = applyFieldOverrides({
data: [dataFrame],
fieldConfig: {
overrides: [],
defaults: {},
},
theme: createTheme(),
replaceVariables: (value: string) => value,
});
return dataFrames[0];
}
function onDeleteClick(e: DataLinkClickEvent, tasks: TaskTableRowConfig[], setTasks: any): void {
const rowIndex = e.origin.rowIndex;
const filteredTasks = _.filter(tasks, (task, idx) => idx !== rowIndex);
setTasks(filteredTasks);
}
function onDatasourceSelectClick(
e: DataLinkClickEvent,
datasources: DatasourceTableRowConfig[],
setDatasources: any
): void {
const rowIndex = e.origin.rowIndex;
const updatedDatasources = _.clone(datasources);
updatedDatasources[rowIndex].select = !updatedDatasources[rowIndex].select;
setDatasources(updatedDatasources);
}
function createTaskFromDatasource(config: DatasourceTableRowConfig): TaskTableRowConfig {
return {
timestamp: Date.now(),
user: 'admin',
datasource: config.datasource,
rowsCount: 3214,
progress: 100,
status: 'finished',
};
}
function createDatasourceConfig(obj: any, timerange: [number, number]): DatasourceTableRowConfig {
return {
select: false,
panel: 'Panel',
refId: obj.refId,
datasource: obj.datasource,
rawSql: obj.rawSql,
uid: obj.uid,
timerange,
};
}
function convertTimestampToDate(timestamp: number): string {
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
};
return new Date(timestamp).toLocaleString('en-GB', options);
}
const CLOSE_ICON_BASE_64 =
'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACJSURBVHgB7ZPRCcAgDESv0lWc0zhC53OIjmAVUpCSamL76YEoyfFIiAGWfldK6SwnKHyhep9xJ3iPcqgH5RyxV1VlBWYJypXVHMEiCaqBbRhAy3W3B76j954wq6ZSVZsOY+WXt6i9l2ymGTlUq0VpOcIqaQC96Zth01DN1zBBefVI4SNp9Za+6wLcH6DKFrfpxgAAAABJRU5ErkJggg==';
const DOWNLOAD_ICON_BASE_64 =
'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADlSURBVHgB3ZPNDYJAEIVnN96lBA7A2RIsATuQiiwBSrAD7MAjCZBACXqFwPomMRtEwMVwgZeQhfn5mNnMEG1CaZoeiqKwTWJ3JkFCiEtVVU+8+r9iJRlKSrk3iqOFtWJglmXHf3yDwCRJbBxxnudh34cRYluMMbKGcgWNCIlnjEuIJ1JK2WzDWeKb7YHjONEsYBf6kTABY+mWuYX+3Xiex9UFU7D3FllfwLqueQvi/h8ZCtBprDLY703T6A0yWj2ArmSoxedQV4jSSz5xj4pmCi0/NKfrwNz5bdtaNENciOu6N1qNXhzZXHMb9Q+nAAAAAElFTkSuQmCC';
const UNSELECT_ICON_BASE_64 =
'PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgcng9IjEuNSIgZmlsbD0iIzExMTIxNiIgc3Ryb2tlPSIjMkQyRTM0Ii8+Cjwvc3ZnPgo=';
const SELECT_ICON_BASE_64 =
'PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiByeD0iMiIgZmlsbD0iIzRBNzFEMiIvPgo8cGF0aCBkPSJNNS45ODI2NCAxMi41MjE3QzUuNzczOTUgMTIuNTIxNyA1LjU4MjY0IDEyLjQ1MjIgNS40MjYxMiAxMi4yOTU2TDEuOTY1MjUgOC44MzQ3NkMxLjY1MjIxIDguNTIxNzIgMS42NTIyMSA4LjAzNDc2IDEuOTY1MjUgNy43MjE3MkMyLjI3ODI5IDcuNDA4NjcgMi43NjUyNSA3LjQwODY3IDMuMDc4MjkgNy43MjE3Mkw2LjAwMDAzIDEwLjYyNjFMMTIuOTM5MiAzLjcwNDMzQzEzLjI1MjIgMy4zOTEyOCAxMy43MzkyIDMuMzkxMjggMTQuMDUyMiAzLjcwNDMzQzE0LjM2NTMgNC4wMTczNyAxNC4zNjUzIDQuNTA0MzMgMTQuMDUyMiA0LjgxNzM3TDYuNTU2NTYgMTIuMjk1NkM2LjM4MjY0IDEyLjQ1MjIgNi4xOTEzNCAxMi41MjE3IDUuOTgyNjQgMTIuNTIxN1YxMi41MjE3WiIgZmlsbD0iI0ZFRkZGRiIvPgo8L3N2Zz4K';

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

@ -1,20 +1,28 @@
import { DataQuery, DataSourceRef, DataSourceSettings, PanelModel } from '@grafana/data';
export interface PanelOptions {}
export type TaskTableRowConfig = {
timestamp: number;
user: string;
datasource: string;
username: string;
datasourceRef: DataSourceRef;
rowsCount: number;
progress: number;
status: string;
filename?: string;
};
export type DatasourceTableRowConfig = {
select: boolean;
panel: string;
refId: string;
datasource: string;
rawSql: string;
uid: string;
timerange: [number, number];
export type QueryTableRowConfig = Omit<DataQuery, 'datasource'> & {
selected: boolean;
panel: PanelModel;
datasource: DataSourceSettings;
};
export enum DatasourceType {
INFLUXDB = 'influxdb',
GRAPHITE = 'graphite',
PROMETHEUS = 'prometheus',
POSTGRES = 'postgres',
ELASTICSEARCH = 'elasticsearch',
MYSQL = 'mysql',
}

6
src/plugin_state.ts

@ -1,4 +1,4 @@
import { makeRequest } from './services/network_service';
import { queryApi } from './services/api_service';
import {
DataExporterAppPluginMeta,
DataExporterPluginMetaJSONData,
@ -114,7 +114,7 @@ class PluginState {
// TODO: try to disable success alerts from Grafana API
const { key: grafanaToken } = await this.createGrafanaToken();
await this.updateGrafanaPluginSettings({ secureJsonData: { grafanaToken } });
const dataExporterAPIResponse = await makeRequest<RT>(`/connect`, {
const dataExporterAPIResponse = await queryApi<RT>(`/connect`, {
method: 'POST',
});
return { grafanaToken, dataExporterAPIResponse };
@ -165,7 +165,7 @@ class PluginState {
dataExporterApiUrl: string
): Promise<PluginConnectedStatusResponse | string> => {
try {
const resp = await makeRequest<PluginConnectedStatusResponse>(`/status`, {
const resp = await queryApi<PluginConnectedStatusResponse>(`/status`, {
method: 'GET',
});

78
src/services/api_service.ts

@ -0,0 +1,78 @@
import { TaskTableRowConfig } from '../panels/corpglory-dataexporter-panel/types';
import axios from 'axios';
import * as _ from 'lodash';
export const API_HOST = `${window.location.protocol}//${window.location.host}/`;
export const API_PROXY_PREFIX = 'api/plugin-proxy/corpglory-dataexporter-app';
export const API_PATH_PREFIX = '/api';
const instance = axios.create();
instance.interceptors.request.use(function (config) {
config.validateStatus = (status) => {
return status >= 200 && status < 300; // default
};
return {
...config,
};
});
interface RequestConfig {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS';
params?: any;
data?: any;
withCredentials?: boolean;
validateStatus?: (status: number) => boolean;
}
export const queryApi = async <RT = any>(path: string, config: RequestConfig) => {
const { method = 'GET', params, data, validateStatus } = config;
const url = `${API_PROXY_PREFIX}${API_PATH_PREFIX}${path}`;
const response = await instance({
method,
url,
params,
data,
validateStatus,
});
return response.data as RT;
};
export async function getTasks(): Promise<TaskTableRowConfig[]> {
return queryApi<TaskTableRowConfig[]>('/task', {});
}
export async function deleteTask(filename?: string): Promise<void> {
if (_.isEmpty(filename)) {
console.warn(`can't delete task without filename`);
return;
}
await queryApi<TaskTableRowConfig[]>('/task', { method: 'DELETE', data: { filename } });
}
export async function getStaticFile(filename?: string): Promise<void> {
if (_.isEmpty(filename)) {
console.warn(`can't download file without name`);
return;
}
const respData = await queryApi(`/static/${filename}.csv`, {});
// TODO: check if resp exists
// create file link in browser's memory
const href = URL.createObjectURL(new Blob([respData], { type: 'text/csv' }));
// create "a" HTML element with href to file & click
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${filename}.csv`);
document.body.appendChild(link);
link.click();
// clean up "a" element & remove ObjectURL
document.body.removeChild(link);
URL.revokeObjectURL(href);
}

11
src/services/grafana_backend_service.ts

@ -0,0 +1,11 @@
import { getBackendSrv } from '@grafana/runtime';
export async function getDatasources() {
const backendSrv = getBackendSrv();
return backendSrv.get(`/api/datasources`);
}
export async function getDashboardByUid(uid: string) {
const backendSrv = getBackendSrv();
return backendSrv.get(`/api/dashboards/uid/${uid}`);
}

41
src/services/network_service.ts

@ -1,41 +0,0 @@
import axios from 'axios';
export const API_HOST = `${window.location.protocol}//${window.location.host}/`;
export const API_PROXY_PREFIX = 'api/plugin-proxy/corpglory-dataexporter-app';
export const API_PATH_PREFIX = '/api';
const instance = axios.create();
instance.interceptors.request.use(function (config) {
config.validateStatus = (status) => {
return status >= 200 && status < 300; // default
};
return {
...config,
};
});
interface RequestConfig {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS';
params?: any;
data?: any;
withCredentials?: boolean;
validateStatus?: (status: number) => boolean;
}
export const makeRequest = async <RT = any>(path: string, config: RequestConfig) => {
const { method = 'GET', params, data, validateStatus } = config;
const url = `${API_PROXY_PREFIX}${API_PATH_PREFIX}${path}`;
const response = await instance({
method,
url,
params,
data,
validateStatus,
});
return response.data as RT;
};

21
src/utils/index.ts

@ -5,3 +5,24 @@ import appEvents from 'grafana/app/core/app_events';
export function openNotification(message: React.ReactNode) {
appEvents.emit(AppEvents.alertSuccess, [message]);
}
export function getDashboardUid(url: string): string {
const matches = new URL(url).pathname.match(/\/d\/([^/]+)/);
if (!matches) {
throw new Error(`Couldn't parse uid from ${url}`);
} else {
return matches[1];
}
}
export function convertTimestampToDate(timestamp: number): string {
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
};
return new Date(timestamp).toLocaleString('en-GB', options);
}

4
yarn.lock

@ -3592,6 +3592,10 @@
dependencies:
"@types/node" "*"
"@types/grafana@github:CorpGlory/types-grafana":
version "4.6.3"
resolved "https://codeload.github.com/CorpGlory/types-grafana/tar.gz/4beede5fa0e5bdebdb23b09e4e1e214dcc4c267f"
"@types/history@^4.7.11":
version "4.7.11"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"

Loading…
Cancel
Save