diff --git a/package.json b/package.json index d44c232..828f7cd 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "fast-csv": "^2.5.0", "lodash": "^4.17.21", "moment": "^2.29.4", + "moment-timezone": "^0.5.40", "nodemon": "^2.0.20", "ts-loader": "^9.4.2", "typescript": "^4.9.4", diff --git a/src/models/target.ts b/src/models/target.ts deleted file mode 100644 index ca24bdf..0000000 --- a/src/models/target.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { DataSourceSettings, PanelModel } from '../types'; - -export class Target { - constructor( - public panel: PanelModel, - public datasource: DataSourceSettings, - ) {} -} diff --git a/src/routes/tasks.ts b/src/routes/tasks.ts index 1d3c702..ecf19be 100644 --- a/src/routes/tasks.ts +++ b/src/routes/tasks.ts @@ -14,6 +14,7 @@ type TRequest = { body: { task: ExportTask, url: string, + timeZoneName: string, }, }; @@ -67,7 +68,7 @@ async function addTask(req: TRequest, res) { } const exporter = exporterFactory.getExporter(); - exporter.export(task, datasourceUrl); + exporter.export(task, datasourceUrl, body.timeZoneName); res.status(200).send(`Export process started`); } diff --git a/src/services/exporter.ts b/src/services/exporter.ts index e35bbb3..8d63013 100644 --- a/src/services/exporter.ts +++ b/src/services/exporter.ts @@ -1,7 +1,6 @@ -import { Target } from '../models/target'; import { URL } from 'url'; import { apiKeys } from '../config'; -import { promisify } from '../utils'; +import { promisify, toIsoString } from '../utils'; import { DashboardQuery, ExportProgress, ExportStatus, ExportTask } from '../types'; import { QueryConfig, queryByConfig } from '@corpglory/tsdb-kit'; @@ -27,7 +26,7 @@ const DEFAULT_PROGRESS = { export class Exporter { private _task: ExportTask; - public async export(task: ExportTask, datasourceUrl: string) { + public async export(task: ExportTask, datasourceUrl: string, timeZoneName: string) { try { this._task = _.cloneDeep(task); this._task.id = uuidv4(); @@ -36,22 +35,17 @@ export class Exporter { this._validateQueries(task.queries); this._validateDatasourceUrl(datasourceUrl); - const targets = task.queries.map((query: DashboardQuery) => new Target( - query.panel, - query.datasource, - )); - await this._updateProgress(); - const queryConfigs = targets.map( - target => + const queryConfigs = task.queries.map( + query => new QueryConfig( QueryType.GRAFANA, { - ...target.datasource, - url: datasourceUrl + ...query.datasource, + url: datasourceUrl, }, - target.panel.targets + [query.target] ) ); @@ -76,12 +70,31 @@ export class Exporter { const datasourceMetrics = await queryByConfig(queryConfig, datasourceUrl, from, to, apiKey); - columns = datasourceMetrics.columns; - values = datasourceMetrics.values; + if(_.isEmpty(columns)) { + columns = datasourceMetrics.columns; + } else { + columns = _.concat(columns, datasourceMetrics.columns.slice(1)); + } + + if(_.isEmpty(values)) { + values = datasourceMetrics.values; + } else { + if(values.length !== datasourceMetrics.values.length) { + throw new Error(`All queries should return rows of the same lengths`); + } + for(const rowIdx in values) { + if(datasourceMetrics.values[rowIdx][0] !== values[rowIdx][0]) { + throw new Error('Queries should return the same timestamps'); + } + values[rowIdx] = _.concat(values[rowIdx], datasourceMetrics.values[rowIdx].slice(1)); + } + } } + values = values.map((row: number[]) => [toIsoString(row[0], timeZoneName), ...row.slice(1)]); + if(columns.length > 0) { - this._writeCsv(stream, { columns, values, }); + this._writeCsv(stream, { columns, values }); } await this._updateProgress({ status: ExportStatus.EXPORTING, progress: (day + 1) / days }); @@ -137,8 +150,8 @@ export class Exporter { if(_.isEmpty(query.datasource)) { throw new Error('all queries should have a `datasource` field'); } - if(_.isEmpty(query.panel.targets)) { - throw new Error('all queries should have a `panel` field with non-empty `targets` field'); + if(_.isEmpty(query.target)) { + throw new Error('all queries should have a `target` field'); } } } diff --git a/src/types.ts b/src/types.ts index 05aa249..16ae109 100644 --- a/src/types.ts +++ b/src/types.ts @@ -105,8 +105,9 @@ export type ExportTask = { id?: string; }; -export type DashboardQuery = DataQuery & { +export type DashboardQuery = { selected: boolean; + target: DataQuery; panel: PanelModel; datasource: DataSourceSettings; }; diff --git a/src/utils.ts b/src/utils.ts index 094d51c..a1fcc7f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import * as moment from 'moment-timezone'; + export async function promisify(method: (...params: any[]) => Promise | void, ...params: any[]) { return new Promise((resolve, reject) => { method(...params, (err, result) => { @@ -9,3 +11,7 @@ export async function promisify(method: (...params: any[]) => Promise | voi }) }); } + +export function toIsoString(msTimestamp: number, timeZone: string): string { + return moment.tz(msTimestamp, timeZone).format('YYYY-MM-DD HH:mm:ssZ').replace(/:00$/, ''); +} diff --git a/yarn.lock b/yarn.lock index 5462164..9d920c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1046,7 +1046,14 @@ minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -moment@^2.22.2, moment@^2.29.4: +moment-timezone@^0.5.40: + version "0.5.40" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.40.tgz#c148f5149fd91dd3e29bf481abc8830ecba16b89" + integrity sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.22.2, moment@^2.29.4: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==