Compare commits

...

32 Commits
dist ... master

Author SHA1 Message Date
rozetko 3b8bb6bf10 1.0.10 11 months ago
rozetko 3febdc8f77 upd readme 11 months ago
rozetko 75c383299b upd description 11 months ago
rozetko 1ec352d4fe plugin.json: add screenshots 11 months ago
rozetko 67cd652cad upd links 11 months ago
rozetko 9a48b1df67 Merge pull request 'Unique API key names' (#19) from unique-api-key-names into master 11 months ago
rozetko d016a3b4e6 1.0.9 11 months ago
rozetko 5a223eddb1 generate unique API key names 11 months ago
rozetko 3079ce5936 upd links 11 months ago
rozetko cb7982245a 1.0.8 11 months ago
rozetko 507757bc72 Merge pull request 'fix panel not working in grafana 10' (#18) from fix-panel-not-working-in-grafana-10 into master 11 months ago
rozetko 2f80b19690 fix panel not working in grafana 10 11 months ago
rozetko de2bafe7d7 upd links 1 year ago
rozetko 95cef43253 1.0.7 1 year ago
rozetko 562eb7798a Merge pull request 'support grafana 9.4.0+' (#17) from api-tokens-for-non-original-organizations into master 1 year ago
rozetko a57f704113 hotfix 1 year ago
vargburz 1e121c4b75 generate api token on each task creation 1 year ago
vargburz b4957048cb support grafana 9.4.0 1 year ago
rozetko 1c6549412a Plugin Configuration docs 1 year ago
rozetko bc3313515d upd docs 1 year ago
rozetko 508a9ccb41 upd docs 1 year ago
rozetko 82f3bc3429 upd docs 1 year ago
rozetko 5952d799df upd docs 1 year ago
rozetko 20d903f59f Merge pull request 'wrap up' (#16) from remove-columns into master 1 year ago
rozetko 3f0f1f99cb hotfix 1 year ago
rozetko 15e0271021 1.0.5 1 year ago
rozetko ae52125e5c download csv with a proper filename && replace alert-errors with warnings 1 year ago
rozetko 6c6c0ec2a4 1.0.4 1 year ago
rozetko ce59479b13 Panel: remove Status Updated At and User columns 1 year ago
vargburz abc2bece5c Merge pull request 'delimiter && download icon && editor permission' (#15) from selector-with-delimeter into master 1 year ago
vargburz 0cb27a3053 1.0.3 1 year ago
vargburz 5563e7f91a delimiter && download icon && editor permission 1 year ago
  1. 95
      README.md
  2. 4
      package.json
  3. 6
      src/icons.ts
  4. BIN
      src/img/task-creation.png
  5. BIN
      src/img/task-list.png
  6. 166
      src/panels/corpglory-dataexporter-panel/components/Panel.tsx
  7. 2
      src/panels/corpglory-dataexporter-panel/types.ts
  8. 15
      src/plugin.json
  9. 24
      src/plugin_state.ts
  10. 21
      src/services/api_service.ts
  11. 7
      yarn.lock

95
README.md

@ -1,79 +1,36 @@
# Grafana app plugin template
# Grafana Data Exporter App
This template is a starting point for building an app plugin for Grafana.
Grafana plugin for exporting data from Grafana panels as CSV.
## What are Grafana app plugins?
Supported datasources:
- MySQL
- PostgreSQL
App plugins can let you create a custom out-of-the-box monitoring experience by custom pages, nested datasources and panel plugins.
We work on expanding this list. If you would like us to support any particular datasource -- please let us know at ping@corpglory.com
## Getting started
## Prerequisites
- [Grafana 9.0.0+](https://grafana.com/grafana/download)
- [Grafana Data Exporter](https://code.corpglory.net/corpglory/grafana-data-exporter)
### Frontend
## Plugin Configuration
1. Install dependencies
- Make sure [Grafana Data Exporter](https://code.corpglory.net/corpglory/grafana-data-exporter) is running, and accessible from Grafana Server
- In Grafana, go to Configuration -> Plugins -> Data Exporter App
- Fill "DataExporter backend URL" field with the Data Exporter URL (Please note: the URL should be accessible from Grafana Server)
- Click Connect
- If Grafana connects to the Data Exporter successfully, you'll see this message: "Plugin is connected! You can now go to a dashboard and add the DataExporter panel there."
```bash
yarn install
```
## Plugin Usage
- go to a dashboard you'd like to export data from
- click "Add panel"
- select Data Exporter Panel
- click Add Task
- select timerange and query
- click Export
2. Build plugin in development mode or run in watch mode
## Support and Consulting
```bash
yarn dev
Commercial support, professional services **or any help** — send us your inquiry at ping@corpglory.com
# or
yarn watch
```
3. Build plugin in production mode
```bash
yarn build
```
4. Run the tests (using Jest)
```bash
# Runs the tests and watches for changes
yarn test
# Exists after running all the tests
yarn lint:ci
```
5. Spin up a Grafana instance and run the plugin inside it (using Docker)
```bash
yarn server
```
6. Run the E2E tests (using Cypress)
```bash
# Spin up a Grafana instance first that we tests against
yarn server
# Start the tests
yarn e2e
```
7. Run the linter
```bash
yarn lint
# or
yarn lint:fix
```
## Learn more
Below you can find source code for existing app plugins and other related documentation.
- [Basic app plugin example](https://github.com/grafana/grafana-plugin-examples/tree/master/examples/app-basic#readme)
- [Plugin.json documentation](https://grafana.com/docs/grafana/latest/developers/plugins/metadata/)
- [How to sign a plugin?](https://grafana.com/docs/grafana/latest/developers/plugins/sign-a-plugin/)
## About CorpGlory Inc.
Grafana Data Exporter is developed by [CorpGlory Inc.](https://corpglory.com/), a company which provides high quality software development, data visualization, Grafana and monitoring consulting.

4
package.json

@ -1,6 +1,6 @@
{
"name": "corpglory-dataexporter-app",
"version": "1.0.2",
"version": "1.0.10",
"description": "",
"scripts": {
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 ./src",
@ -55,6 +55,7 @@
"@types/node": "^17.0.19",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-router-dom": "^5.3.3",
"@types/uuid": "^9.0.2",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"axios": "^1.2.1",
@ -84,6 +85,7 @@
"ts-node": "^10.5.0",
"tsconfig-paths": "^3.12.0",
"typescript": "^4.4.0",
"uuid": "^9.0.0",
"webpack": "^5.69.1",
"webpack-cli": "^4.9.2",
"webpack-livereload-plugin": "^3.0.2"

6
src/icons.ts

@ -6,5 +6,9 @@ export const UNSELECT_ICON_BASE_64 =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgcng9IjEuNSIgZmlsbD0iIzExMTIxNiIgc3Ryb2tlPSIjMkQyRTM0Ii8+Cjwvc3ZnPgo=';
export const SELECT_ICON_BASE_64 =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiByeD0iMiIgZmlsbD0iIzRBNzFEMiIvPgo8cGF0aCBkPSJNNS45ODI2NCAxMi41MjE3QzUuNzczOTUgMTIuNTIxNyA1LjU4MjY0IDEyLjQ1MjIgNS40MjYxMiAxMi4yOTU2TDEuOTY1MjUgOC44MzQ3NkMxLjY1MjIxIDguNTIxNzIgMS42NTIyMSA4LjAzNDc2IDEuOTY1MjUgNy43MjE3MkMyLjI3ODI5IDcuNDA4NjcgMi43NjUyNSA3LjQwODY3IDMuMDc4MjkgNy43MjE3Mkw2LjAwMDAzIDEwLjYyNjFMMTIuOTM5MiAzLjcwNDMzQzEzLjI1MjIgMy4zOTEyOCAxMy43MzkyIDMuMzkxMjggMTQuMDUyMiAzLjcwNDMzQzE0LjM2NTMgNC4wMTczNyAxNC4zNjUzIDQuNTA0MzMgMTQuMDUyMiA0LjgxNzM3TDYuNTU2NTYgMTIuMjk1NkM2LjM4MjY0IDEyLjQ1MjIgNi4xOTEzNCAxMi41MjE3IDUuOTgyNjQgMTIuNTIxN1YxMi41MjE3WiIgZmlsbD0iI0ZFRkZGRiIvPgo8L3N2Zz4K';
export const DOWNLOADING_ICON_BASE_64 =
export const PRELOADER_ICON_BASE_64 =
'data:image/gif;base64,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';
export const DOWNLOAD_ICON_BASE_64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADlSURBVHgB3ZPNDYJAEIVnN96lBA7A2RIsATuQiiwBSrAD7MAjCZBACXqFwPomMRtEwMVwgZeQhfn5mNnMEG1CaZoeiqKwTWJ3JkFCiEtVVU+8+r9iJRlKSrk3iqOFtWJglmXHf3yDwCRJbBxxnudh34cRYluMMbKGcgWNCIlnjEuIJ1JK2WzDWeKb7YHjONEsYBf6kTABY+mWuYX+3Xiex9UFU7D3FllfwLqueQvi/h8ZCtBprDLY703T6A0yWj2ArmSoxedQV4jSSz5xj4pmCi0/NKfrwNz5bdtaNENciOu6N1qNXhzZXHMb9Q+nAAAAAElFTkSuQmCC';
export const DISABLED_DOWNLOAD_ICON_BASE_64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAPNJREFUOE9jZKAyYKSyeQxEGXjr1i0DVlbWD4qKig8IOYAoA4NDI/6DDFq7egVB9QQVgAwaNRBvvOAMw9u3bzuoqqoewBaGyHLopmM18Pr16wo1dY33YTGLHCkwdk9Xu6CiouIHogwEKbp161ZCZXXdfGz+62xvSVRRUVmATQ5vssFmKD7DQBbADbx//77Anz9/DGDhBrMd2VBshkFz0QOY9+EGwjRiyw137twJYGBgEMDmTVCYIltElIH40gn9DASlrYqq2v2EShNs8m0tjYHq6uobUCIFxAGF1b9//wRIMZSRkfEBckQSVdqQZAEpiolRCwC5RpEVeMR8nwAAAABJRU5ErkJggg==';

BIN
src/img/task-creation.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
src/img/task-list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

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

@ -11,21 +11,23 @@ import {
import { convertTimestampToDate, convertTimeZoneTypeToName, getCurrentDashboardUid } from '../../../utils';
import {
CLOSE_ICON_BASE_64,
DOWNLOADING_ICON_BASE_64,
OPTIONS_ICON_BASE_64,
PRELOADER_ICON_BASE_64,
SELECT_ICON_BASE_64,
UNSELECT_ICON_BASE_64,
DOWNLOAD_ICON_BASE_64,
DISABLED_DOWNLOAD_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 {
Table,
Button,
Select,
HorizontalGroup,
VerticalGroup,
Modal,
@ -46,11 +48,14 @@ import {
DataSourceSettings,
TimeRange,
OrgRole,
SelectableValue,
AppEvents,
} from '@grafana/data';
import { RefreshEvent } from '@grafana/runtime';
import React, { useState, useEffect } from 'react';
import * as _ from 'lodash';
import PluginState from 'plugin_state';
const PANEL_ID = 'corpglory-dataexporter-panel';
const APP_ID = 'corpglory-dataexporter-app';
@ -70,6 +75,8 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
const [isModalOpen, setModalVisibility] = useState<boolean>(false);
const [csvDelimiter, setCsvDelimiter] = useState<string>(',');
const [selectedTimeRange, setTimeRange] = useState<TimeRange>(timeRange);
const [panelStatus, setPanelStatus] = useState<PanelStatus>(PanelStatus.LOADING);
@ -84,7 +91,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
const timeZoneName = convertTimeZoneTypeToName(timeZone);
if (contextSrv.user.orgRole !== OrgRole.Admin) {
if (contextSrv.user.orgRole === OrgRole.Viewer) {
setPanelStatusWithValidate(PanelStatus.PERMISSION_ERROR);
}
@ -207,7 +214,9 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
to: timerange[1] * 1000,
},
queries: selectedQueries,
csvDelimiter,
};
const token = await PluginState.createGrafanaToken();
// TODO: move this function to API Service
await queryApi('/task', {
method: 'POST',
@ -215,6 +224,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
task,
url: window.location.toString(),
timeZoneName,
apiKey: token.key,
},
});
@ -254,6 +264,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
custom: {
filterable: false,
displayMode: 'image',
cellOptions: { type: 'image' },
},
links: [
{
@ -301,9 +312,39 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
name: 'A',
fields: [
{
name: 'Status Updated At',
type: FieldType.number,
values: _.map(sortedTasks, (task) => convertTimestampToDate(task.progress?.time)),
name: 'Download',
type: FieldType.string,
values: _.map(sortedTasks, (task) => {
switch (task.progress?.status) {
case ExportStatus.FINISHED:
if (task.id === taskIdBeingDownloaded) {
return PRELOADER_ICON_BASE_64;
} else {
return DOWNLOAD_ICON_BASE_64;
}
case ExportStatus.ERROR:
return DISABLED_DOWNLOAD_ICON_BASE_64;
case ExportStatus.EXPORTING:
return PRELOADER_ICON_BASE_64;
default:
throw new Error(`Unknown exporting status: ${task.progress?.status}`);
}
}),
config: {
custom: {
filterable: false,
displayMode: 'image',
cellOptions: { type: 'image' },
},
links: [
{
targetBlank: false,
title: 'Download',
url: '#',
onClick: (event: DataLinkClickEvent) => onDownloadClick(event),
},
],
},
},
{
name: 'From',
@ -315,16 +356,6 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
type: FieldType.number,
values: _.map(sortedTasks, (task) => convertTimestampToDate(task.timeRange.to)),
},
{
name: 'User',
type: FieldType.string,
values: _.map(sortedTasks, (task) => task.username),
},
{
name: 'Datasource',
type: FieldType.string,
values: _.map(sortedTasks, (task) => task.queries.map((query) => query.datasource?.name).join(',')),
},
{
name: 'Exported Rows',
type: FieldType.number,
@ -346,20 +377,20 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
values: _.map(sortedTasks, (task) => task.progress?.errorMessage || '-'),
},
{
name: 'Actions',
name: 'Delete',
type: FieldType.string,
values: _.map(sortedTasks, (task) => {
switch (task.progress?.status) {
case ExportStatus.FINISHED:
if (task.id === taskIdBeingDownloaded) {
return DOWNLOADING_ICON_BASE_64;
return PRELOADER_ICON_BASE_64;
} else {
return OPTIONS_ICON_BASE_64;
return CLOSE_ICON_BASE_64;
}
case ExportStatus.ERROR:
return CLOSE_ICON_BASE_64;
case ExportStatus.EXPORTING:
return '';
return PRELOADER_ICON_BASE_64;
default:
throw new Error(`Unknown exporting status: ${task.progress?.status}`);
}
@ -368,7 +399,16 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
custom: {
filterable: false,
displayMode: 'image',
cellOptions: { type: 'image' },
},
links: [
{
targetBlank: false,
title: 'Delete',
url: '#',
onClick: (event: DataLinkClickEvent) => onDeleteClick(event),
},
],
},
},
],
@ -384,57 +424,37 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
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(taskToDelete: ExportTask): Promise<void> {
async function onDeleteClick(event: DataLinkClickEvent): Promise<void> {
const rowIndex = event.origin.rowIndex;
const sortedTasks = _.orderBy(tasks, (task) => task.progress?.time, 'desc');
const taskToDelete = sortedTasks[rowIndex];
if (taskToDelete.progress?.status === ExportStatus.EXPORTING || taskToDelete.id === taskIdBeingDownloaded) {
appEvents.emit(AppEvents.alertWarning, ['Data Exporter', 'Active task can`t be deleted']);
return;
}
await deleteTask(taskToDelete?.id);
const filteredTasks = _.filter(tasks, (task) => task.id !== taskToDelete.id);
setTasks(filteredTasks);
}
async function onDownloadClick(task: ExportTask): Promise<void> {
async function onDownloadClick(event: DataLinkClickEvent): Promise<void> {
const rowIndex = event.origin.rowIndex;
const sortedTasks = _.orderBy(tasks, (task) => task.progress?.time, 'desc');
const task = sortedTasks[rowIndex];
if (task.progress?.status === ExportStatus.EXPORTING || task.id === taskIdBeingDownloaded) {
appEvents.emit(AppEvents.alertWarning, ['Data Exporter', 'Active task can`t be downloaded']);
return;
}
if (task.progress?.status === ExportStatus.ERROR) {
appEvents.emit(AppEvents.alertWarning, ['Data Exporter', 'Failed task can`t be downloaded']);
return;
}
setTaskIdBeingDownloaded(task.id as string);
await getStaticFile(task?.id);
await getStaticFile(task?.filename);
setTaskIdBeingDownloaded(null);
}
@ -448,6 +468,12 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
setQueries(updatedQueries);
}
const delimeterOptions: SelectableValue[] = [
{ value: ',', label: 'Comma: ","' },
{ value: ';', label: 'Semicolon: ";"' },
{ value: '\t', label: 'Tabulator: "\\t"' },
];
const styles = useStyles2(getStyles);
const loadingDiv = <LoadingPlaceholder text="Loading..."></LoadingPlaceholder>;
@ -468,12 +494,12 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
const permissionErrorDiv = (
<div>
<p> Permission Error. </p>
<div> DataExporter panel available only for Admins. </div>
<div> DataExporter panel available only for Admins and Editors. </div>
</div>
);
const mainDiv = (
<div>
<Table width={width} height={height - 40} data={tasksDataFrame as DataFrame} />
{tasksDataFrame && <Table width={width} height={height - 40} data={tasksDataFrame as DataFrame} />}
<HorizontalGroup justify="flex-end">
<Button variant="primary" icon="plus" style={{ marginTop: '8px' }} onClick={openDatasourceModal}>
Add Task
@ -495,7 +521,17 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) {
/>
</HorizontalGroup>
<Table width={width / 2 - 20} height={height - 40} data={queriesDataFrame} />
<HorizontalGroup justify="flex-end" spacing="md">
<HorizontalGroup justify="space-between" spacing="md">
<HorizontalGroup justify="flex-end" spacing="md">
<span>CSV delimiter:</span>
<Select
options={delimeterOptions}
value={csvDelimiter}
onChange={(el) => {
setCsvDelimiter(el.value);
}}
/>
</HorizontalGroup>
<Button
variant="primary"
aria-label="Add task button"

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

@ -40,8 +40,10 @@ export type ExportTask = {
from: number;
to: number;
};
csvDelimiter: string;
progress?: ExportProgress;
id?: string;
filename?: string;
};
export type DashboardQuery = {

15
src/plugin.json

@ -4,7 +4,7 @@
"name": "Data Exporter App",
"id": "corpglory-dataexporter-app",
"info": {
"description": "",
"description": "Export dashboard data into CSV",
"author": {
"name": "CorpGlory Inc."
},
@ -13,7 +13,16 @@
"large": "img/logo.svg"
},
"keywords": ["export", "csv"],
"screenshots": [],
"screenshots": [
{
"name": "Task List",
"path": "img/task-list.png"
},
{
"name": "Task Creation",
"path": "img/task-creation.png"
}
],
"version": "%VERSION%",
"updated": "%TODAY%"
},
@ -31,7 +40,7 @@
}
],
"dependencies": {
"grafanaDependency": ">=7.0.0",
"grafanaDependency": ">=9.0.0",
"plugins": []
}
}

24
src/plugin_state.ts

@ -7,8 +7,12 @@ import {
import { getBackendSrv } from '@grafana/runtime';
import { v4 as uuidv4 } from 'uuid';
import axios from 'axios';
import * as _ from 'lodash';
export type UpdateGrafanaPluginSettingsProps = {
jsonData?: Partial<DataExporterPluginMetaJSONData>;
secureJsonData?: Partial<DataExporterPluginMetaSecureJSONData>;
@ -97,19 +101,25 @@ class PluginState {
static createGrafanaToken = async () => {
const baseUrl = '/api/auth/keys';
const keys = await this.grafanaBackend.get(baseUrl);
const existingKey = keys.find((key: { id: number; name: string; role: string }) => key.name === 'DataExporter');
const keys = await this.grafanaBackend.get(baseUrl, { includeExpired: true });
const existingKeys = keys.filter((key: { id: number; name: string; role: string }) =>
_.includes(key.name, 'DataExporter')
);
if (existingKey) {
// @ts-ignore
// for some reason, there is no `options` argument in Grafana's public types for BackendSrv but it exists
await this.grafanaBackend.delete(`${baseUrl}/${existingKey.id}`, undefined, { showSuccessAlert: false });
console.log('existingKeys', existingKeys);
if (!_.isEmpty(existingKeys)) {
for (let key of existingKeys) {
// @ts-ignore
// for some reason, there is no `options` argument in Grafana's public types for BackendSrv but it exists
await this.grafanaBackend.delete(`${baseUrl}/${key.id}`, undefined, { showSuccessAlert: false });
}
}
return await this.grafanaBackend.post(
baseUrl,
{
name: 'DataExporter',
name: `DataExporter_${uuidv4()}`,
role: 'Admin',
secondsToLive: null,
},

21
src/services/api_service.ts

@ -1,5 +1,8 @@
import { ExportTask } from '../panels/corpglory-dataexporter-panel/types';
import appEvents from 'grafana/app/core/app_events';
import { AppEvents } from '@grafana/data';
import axios from 'axios';
import * as _ from 'lodash';
@ -55,24 +58,20 @@ export async function deleteTask(taskId?: string): Promise<void> {
await queryApi<ExportTask[]>('/task', { method: 'DELETE', data: { taskId } });
}
export async function getStaticFile(taskId?: string): Promise<void> {
if (_.isEmpty(taskId)) {
console.warn(`can't download file without taskId`);
export async function getStaticFile(filename?: string): Promise<void> {
if (_.isEmpty(filename)) {
appEvents.emit(AppEvents.alertWarning, ['Data Exporter', 'Can`t download a file without filename']);
console.warn(`can't download file without filename`);
return;
}
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' }));
// create "a" HTML element with href to file & click
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', `${taskId}.csv`);
link.href = `${API_PROXY_PREFIX}${API_PATH_PREFIX}/static/${filename}.csv`;
link.target = '_blank';
link.download = `${filename}.csv`;
document.body.appendChild(link);
link.click();
// clean up "a" element & remove ObjectURL
document.body.removeChild(link);
URL.revokeObjectURL(href);
}

7
yarn.lock

@ -3945,6 +3945,11 @@
dependencies:
source-map "^0.6.1"
"@types/uuid@^9.0.2":
version "9.0.2"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b"
integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==
"@types/webpack-dev-server@3":
version "3.11.6"
resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.6.tgz#d8888cfd2f0630203e13d3ed7833a4d11b8a34dc"
@ -12944,7 +12949,7 @@ uuid@8.3.2, uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@9.0.0:
uuid@9.0.0, uuid@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==

Loading…
Cancel
Save