@opengis/bi 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bi.js +1 -1
- package/dist/bi.umd.cjs +58 -58
- package/dist/{import-file-DUp3rsNI.js → import-file-BqdbrKVj.js} +6310 -6238
- package/dist/{map-component-mixin-CGM0P5ub.js → map-component-mixin-BeW3TYyl.js} +2 -2
- package/dist/{vs-calendar-cOoinEwc.js → vs-calendar-CnosX1Ss.js} +20 -9
- package/dist/{vs-funnel-bar-kLkPoIhJ.js → vs-funnel-bar-CcXr5oIQ.js} +2 -2
- package/dist/{vs-heatmap-3XAVGTSo.js → vs-heatmap-DvOx7wot.js} +3 -4
- package/dist/{vs-map-B1tr6V5_.js → vs-map-C2KEv_W6.js} +2 -2
- package/dist/{vs-map-cluster-BWJPx7wE.js → vs-map-cluster-DJpEG5n8.js} +2 -2
- package/dist/{vs-number-CrU7LmkV.js → vs-number-C23hqXxA.js} +19 -12
- package/dist/{vs-text-DRPx3aID.js → vs-text-COR-T0yn.js} +18 -13
- package/package.json +18 -8
- package/plugin.js +1 -0
- package/server/migrations/bi.dataset.sql +26 -0
- package/server/routes/dashboard/controllers/dashboard.delete.js +2 -1
- package/server/routes/dashboard/controllers/dashboard.js +29 -25
- package/server/routes/dashboard/controllers/dashboard.list.js +3 -9
- package/server/routes/data/controllers/data.js +21 -10
- package/server/routes/data/controllers/util/chartSQL.js +6 -3
- package/server/routes/data/controllers/util/normalizeData.js +9 -6
- package/server/routes/data/index.mjs +7 -2
- package/server/routes/dataset/controllers/bi.dataset.demo.add.js +97 -0
- package/server/routes/dataset/controllers/bi.dataset.import.js +67 -0
- package/server/routes/dataset/controllers/util/create.table.js +22 -0
- package/server/routes/dataset/controllers/util/prepare.data.js +49 -0
- package/server/routes/dataset/index.mjs +19 -0
- package/server/routes/edit/controllers/dashboard.add.js +4 -2
- package/server/routes/edit/controllers/dashboard.edit.js +4 -2
- package/server/routes/edit/controllers/widget.add.js +5 -3
- package/server/routes/edit/controllers/widget.del.js +1 -2
- package/server/routes/edit/controllers/widget.edit.js +27 -78
- package/server/routes/map/controllers/cluster.js +27 -22
- package/server/routes/map/controllers/clusterVtile.js +11 -58
- package/server/routes/map/controllers/geojson.js +8 -8
- package/server/routes/map/controllers/map.js +12 -9
- package/server/routes/map/controllers/utils/downloadClusterData.js +43 -0
- package/server/routes/map/controllers/vtile.js +6 -5
- package/server/utils/getWidget.js +5 -3
|
@@ -15,7 +15,10 @@ function chart({
|
|
|
15
15
|
groupData,
|
|
16
16
|
order,
|
|
17
17
|
samples,
|
|
18
|
+
xType,
|
|
18
19
|
}) {
|
|
20
|
+
const xCol = x && xType?.includes('[]') ? `unnest(${x})` : x;
|
|
21
|
+
|
|
19
22
|
const metricData =
|
|
20
23
|
groupData
|
|
21
24
|
?.map(
|
|
@@ -23,11 +26,11 @@ function chart({
|
|
|
23
26
|
`${metric} filter (where ${groupby}='${el.name}') as "${el.name}"`
|
|
24
27
|
)
|
|
25
28
|
.join(',') || `${metric} as metric`;
|
|
26
|
-
const sql = `select ${
|
|
29
|
+
const sql = `select ${xCol}, ${metricData}
|
|
27
30
|
from ${table}
|
|
28
31
|
where ${where}
|
|
29
|
-
group by ${
|
|
30
|
-
order by ${order ||
|
|
32
|
+
${xCol ? `group by ${xCol}` : ''}
|
|
33
|
+
${order || xCol ? `order by ${order || xCol}` : ''}
|
|
31
34
|
${samples ? 'limit 10' : 'limit 100'}`;
|
|
32
35
|
return sql;
|
|
33
36
|
}
|
|
@@ -15,10 +15,10 @@ function normalizeData(data, query = {}, columnTypes = []) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const xName = query.x || (Array.isArray(data.x) ? data.x[0] : data.x);
|
|
18
|
-
const
|
|
18
|
+
const xType = columnTypes.find((el) => el.name == xName)?.type;
|
|
19
19
|
|
|
20
20
|
const granularity =
|
|
21
|
-
|
|
21
|
+
xType === 'date' || xType?.includes('timestamp')
|
|
22
22
|
? query.granularity || data.granularity || 'year'
|
|
23
23
|
: null;
|
|
24
24
|
|
|
@@ -31,10 +31,13 @@ function normalizeData(data, query = {}, columnTypes = []) {
|
|
|
31
31
|
const metric =
|
|
32
32
|
(query.metric ? `sum(${query.metric})` : null) ||
|
|
33
33
|
(metrics.length
|
|
34
|
-
? metrics
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
? (metrics
|
|
35
|
+
?.filter((el) => el && columnTypes.find((col) => col.name == (el?.name || el)))
|
|
36
|
+
?.map((el) => el.fx || `${el.operator || 'sum'}(${el.name || el})`)?.join(',') || 'count(*)')
|
|
37
37
|
: 'count(*)');
|
|
38
|
+
|
|
39
|
+
const yName = metrics?.[0]?.name || metrics?.[0];
|
|
40
|
+
const yType = columnTypes.find((el) => el.name == yName)?.type;
|
|
38
41
|
|
|
39
42
|
const { cls, table, filterCustom } = data;
|
|
40
43
|
const groupby = query.groupby || data.groupby;
|
|
@@ -51,6 +54,6 @@ function normalizeData(data, query = {}, columnTypes = []) {
|
|
|
51
54
|
? `(select * from ${data?.table} t ${data.tableSQL.join(' \n ')} where ${where})q`
|
|
52
55
|
: undefined;
|
|
53
56
|
|
|
54
|
-
return { x, cls, metric, table, where, tableSQL, groupby, xName };
|
|
57
|
+
return { x, cls, metric, table, where, tableSQL, groupby, xName, xType, yName, yType };
|
|
55
58
|
}
|
|
56
59
|
export default normalizeData;
|
|
@@ -4,9 +4,14 @@ import data from './controllers/data.js';
|
|
|
4
4
|
|
|
5
5
|
const biSchema = {
|
|
6
6
|
querystring: {
|
|
7
|
-
widget: { type: 'string', pattern: '^([\\d\\
|
|
8
|
-
dashboard: { type: 'string', pattern: '^([\\d\\
|
|
7
|
+
widget: { type: 'string', pattern: '^([\\d\\w_]+)$' },
|
|
8
|
+
dashboard: { type: 'string', pattern: '^([\\d\\w_]+)$' },
|
|
9
9
|
sql: { type: 'string', pattern: '^([\\d])$' },
|
|
10
|
+
// metric: { type: 'string', pattern: '^([\\d\\w_]+)$' },
|
|
11
|
+
x: { type: 'string', pattern: '^([\\d\\w_]+)$' },
|
|
12
|
+
granularity: { type: 'string', pattern: '^(week|month|quarter|year)$' },
|
|
13
|
+
groupby: { type: 'string', pattern: '^([\\d\\w_]+)$' },
|
|
14
|
+
filterCustom: { type: 'string', pattern: '^([\\d\\w_,]+)$' },
|
|
10
15
|
},
|
|
11
16
|
params: {
|
|
12
17
|
id: { type: 'string', pattern: '^([\\d\\w]+)$' },
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import { getPG } from '@opengis/fastify-table/utils.js';
|
|
5
|
+
|
|
6
|
+
const host = 'https://cdn.softpro.ua/demo/bi';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const root = process.cwd();
|
|
10
|
+
const send = (msg) => console.log(msg)
|
|
11
|
+
async function downloadFile(fileName) {
|
|
12
|
+
const pg = getPG();
|
|
13
|
+
send(`${fileName} - checking if already downloaded...`);
|
|
14
|
+
|
|
15
|
+
const demoPath = path.join(root, '/log/temp/', fileName);
|
|
16
|
+
// const fileExists = await isFileExists(demoPath);
|
|
17
|
+
|
|
18
|
+
// send(`${host}/${fileName}` + demoPath)
|
|
19
|
+
const response = await fetch(`${host}/${fileName}`);
|
|
20
|
+
const body = await response.text();
|
|
21
|
+
//send(body)
|
|
22
|
+
if (response?.status !== 200) {
|
|
23
|
+
return { [fileName]: { result: 'file not found', status: 404 } };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await mkdir(path.dirname(demoPath), { recursive: true });
|
|
27
|
+
|
|
28
|
+
await writeFile(demoPath, body, 'utf8');
|
|
29
|
+
|
|
30
|
+
await pg.query(`drop table if exists ${path.parse(fileName).name}`);
|
|
31
|
+
const { exists } = await pg.query(`select to_regclass($1) is not null as exists`, [path.parse(fileName).name])
|
|
32
|
+
.then((res1) => res1.rows?.[0] || {});
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if (exists) {
|
|
36
|
+
send(`${fileName} - dataset table already exists!`);
|
|
37
|
+
return { [fileName]: { result: 'dataset table already exists', status: 200 } };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sql = await readFile(demoPath, 'utf8');
|
|
41
|
+
await pg.query(sql);
|
|
42
|
+
return { [fileName]: { result: 'success', status: 200 } };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Імпорт demo даних dashboard
|
|
47
|
+
*
|
|
48
|
+
* @method GET
|
|
49
|
+
* @summary Імпорт demo даних dashboard
|
|
50
|
+
* @priority 4
|
|
51
|
+
* @type api
|
|
52
|
+
* @tag bi
|
|
53
|
+
* @errors 400,500
|
|
54
|
+
* @returns {Number} status Номер помилки
|
|
55
|
+
* @returns {String} error Опис помилки
|
|
56
|
+
* @returns {Object} rows Масив з колонками таблиці
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
const demoList = {
|
|
60
|
+
'demo.orders.sql': 'Статистика продажів (demo)',
|
|
61
|
+
'demo.cleaned_sales_data.sql': 'Статистика продажів (demo 2)',
|
|
62
|
+
'demo.video_game_sales.sql': 'Статистика продажів відеоігор (demo)',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default async function biDatasetDemoAdd(req) {
|
|
66
|
+
const { pg, query = {} } = req;
|
|
67
|
+
// const send = () => { }; // eventStream?
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
const sqlList = [];
|
|
72
|
+
|
|
73
|
+
const tables = Object.keys(demoList)?.map((el) => {
|
|
74
|
+
const table = path.parse(el).name;
|
|
75
|
+
sqlList.push(`insert into bi.dataset(dataset_id,name,table_name)
|
|
76
|
+
select '${el}', '${demoList?.[el]}', '${table}' on conflict(dataset_id) do update set
|
|
77
|
+
name=excluded.name, table_name=excluded.table_name
|
|
78
|
+
returning dataset_id as id`);
|
|
79
|
+
return table;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const { count } = await pg.query(`select count(*) from pg_catalog.pg_tables where schemaname||'.'||tablename = any($1)`, [tables]).then((res1) => res1.rows?.[0] || {});
|
|
83
|
+
|
|
84
|
+
if (count === '3' && !query.nocache) {
|
|
85
|
+
const result = await pg.query(sqlList.join(';\n'));
|
|
86
|
+
const ids = (Array.isArray(result) ? result : [result]).filter((el) => el).map((el) => el?.rows?.[0]?.id);
|
|
87
|
+
return { message: { ids, message: 'all datasets already downloaded' }, status: 200 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const res = {};
|
|
91
|
+
const resultAll = await Promise.all(Object.keys(demoList).map(async (fileName) => await downloadFile(fileName))).catch(err => console.log(err));
|
|
92
|
+
|
|
93
|
+
const result = await pg.query(sqlList.join(';\n'));
|
|
94
|
+
const ids = (Array.isArray(result) ? result : [result]).filter((el) => el).map((el) => el?.rows?.[0]?.id);
|
|
95
|
+
return { message: { ids, message: res, resultAll }, status: 200 };
|
|
96
|
+
|
|
97
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import createTableFunc from './util/create.table.js';
|
|
4
|
+
import prepareData from './util/prepare.data.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Імпорт даних до BI набору
|
|
8
|
+
*
|
|
9
|
+
* @method POST
|
|
10
|
+
* @summary Імпорт даних до BI набору
|
|
11
|
+
* @priority 4
|
|
12
|
+
* @alias biDatasetPost
|
|
13
|
+
* @type api
|
|
14
|
+
* @tag bi
|
|
15
|
+
* @param {Object} params.id Dashboard ID
|
|
16
|
+
* @errors 400,500
|
|
17
|
+
* @returns {Number} status Номер помилки
|
|
18
|
+
* @returns {String} error Опис помилки
|
|
19
|
+
* @returns {Object} rows Масив з колонками таблиці
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export default async function biDatasetPost(req) {
|
|
23
|
+
const {
|
|
24
|
+
pg, params = {}, body = {}, session = {},
|
|
25
|
+
} = req;
|
|
26
|
+
const { uid } = session?.passport?.user || {};
|
|
27
|
+
|
|
28
|
+
if (!uid) {
|
|
29
|
+
return { message: 'access restricted', status: 403 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!Array.isArray(body?.data) && body?.data?.features?.[0]?.type !== 'Feature') {
|
|
33
|
+
return { message: 'body data param is invalid', status: 400 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// await pg.query(`insert into bi.dataset(dataset_id,name, uid) values($1,$1,$2) on conflict (dataset_id) do update set table_name=null`, [params?.id, uid]);
|
|
37
|
+
const dataset = await pg.query('select dataset_id as id, name, table_name as table from bi.dataset where dataset_id=$1', [params?.id]).then((res) => res.rows?.[0] || {});
|
|
38
|
+
if (!dataset.id) {
|
|
39
|
+
return { message: `dataset not found: ${params?.id}`, status: 404 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const tableName = randomBytes(64).toString('hex').substring(0, 24).replace(/^\d+/, '');
|
|
43
|
+
const pkey = tableName.concat('_id');
|
|
44
|
+
const table = `bi_data.${tableName}`;
|
|
45
|
+
const { columns, insertData } = await prepareData({ req, table, data: body?.data });
|
|
46
|
+
const createTable = createTableFunc({ table, pkey, columns });
|
|
47
|
+
const updateDataset = `update bi.dataset set table_name='${table}', editor_id='${uid}' where dataset_id='${params?.id}'`;
|
|
48
|
+
const sqlList = [createTable, insertData, updateDataset];
|
|
49
|
+
if (dataset.name) {
|
|
50
|
+
sqlList.push(`comment on table ${table} is '${dataset.name}'`);
|
|
51
|
+
}
|
|
52
|
+
const q = sqlList.filter((el) => el).join(';');
|
|
53
|
+
await pg.query('create extension if not exists postgis;create schema if not exists bi_data;');
|
|
54
|
+
|
|
55
|
+
// await pg.query(createTable);
|
|
56
|
+
|
|
57
|
+
//const result = {}
|
|
58
|
+
const result = await pg.query(q);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
message: {
|
|
62
|
+
id: params?.id, action: 'import', result: 'success', total: result?.find((el) => el.command === 'INSERT')?.rowCount,
|
|
63
|
+
},
|
|
64
|
+
status: 200,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export default function createTableFunc({
|
|
2
|
+
table, pkey, columns,
|
|
3
|
+
}) {
|
|
4
|
+
const columnType1 = {
|
|
5
|
+
text: 'text', select: 'text', date: 'date', 'yes/no': 'boolean', badge: 'text', number: 'numeric', tags: 'text[]', geom: 'geom',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const createQuery = `create table if not exists ${table} (
|
|
9
|
+
${pkey} text not null default public.uuid_generate_v4(),
|
|
10
|
+
geom public.geometry, ${columns ? `
|
|
11
|
+
${columns?.map((el) => `${el.name} ${columnType1[el.format] || 'text'}`).join(', ')},` : ''}
|
|
12
|
+
cdate timestamp without time zone not null default now(),
|
|
13
|
+
editor_date timestamp without time zone,
|
|
14
|
+
uid text,
|
|
15
|
+
editor_id text,
|
|
16
|
+
files json,
|
|
17
|
+
constraint ${table.replace(/\./g, '_')}_constraint_pkey PRIMARY KEY (${pkey}) )`;
|
|
18
|
+
|
|
19
|
+
const commentQuery = columns?.filter((el) => el.ua)?.map((el) => `comment on column ${table}.${el.name} is '${el.ua}'`).join(';');
|
|
20
|
+
|
|
21
|
+
return [createQuery, commentQuery].join(';');
|
|
22
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { getFolder, isFileExists } from '@opengis/fastify-table/utils.js';
|
|
4
|
+
|
|
5
|
+
import { file2json } from '@opengis/fastify-file/utils.js';
|
|
6
|
+
|
|
7
|
+
function getFileColumns({ data }) {
|
|
8
|
+
if (data?.features) {
|
|
9
|
+
return Object.keys(data?.features[0]?.properties); // geojson / shapefile
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(data)) {
|
|
12
|
+
return Object.keys(data[0])?.filter((el) => el); // csv / xls
|
|
13
|
+
}
|
|
14
|
+
return Object.values(data[Object.keys(data)?.[0]]?.[0]); // else?
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default async function prepareData({
|
|
18
|
+
req, table, file_path: filePath, data: data1,
|
|
19
|
+
}) {
|
|
20
|
+
const rootDir = getFolder(req);
|
|
21
|
+
const fullPath = path.join(rootDir, filePath || '');
|
|
22
|
+
const fileExists = await isFileExists(fullPath);
|
|
23
|
+
if (!fileExists && !data1) {
|
|
24
|
+
return { message: `file not found: ${filePath}`, status: 404 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const data11 = data1 || await file2json(fullPath);
|
|
28
|
+
const data = (['.xls', '.xlsx'].includes(path.extname(fullPath)) && !Array.isArray(data11)) ? data11[Object.keys(data11)[0]] : data11;
|
|
29
|
+
|
|
30
|
+
/* generate column list based on input data */
|
|
31
|
+
const columns = getFileColumns({ data })
|
|
32
|
+
?.filter((el) => !['editor_date', 'cdate', 'uid', 'editor_id', 'files'].includes(el.toLowerCase()))
|
|
33
|
+
?.map((el, index) => ({ name: `col_${index}`, ua: el, format: 'text' }));
|
|
34
|
+
|
|
35
|
+
const columnList1 = columns?.map((el) => el.name)?.join(',');
|
|
36
|
+
const columns1 = ','.concat(columns?.map((el) => `features->'properties'->>'${el?.ua}'`)?.join(','));
|
|
37
|
+
const columns2 = columns?.map((el) => `d->>'${el.ua}'`).join(',');
|
|
38
|
+
const q1 = data?.features ? `(SELECT json_array_elements('${JSON.stringify(data.features).replace(/'/g, "''")}'::json) AS features)q` : null;
|
|
39
|
+
const q2 = !data?.features ? `( SELECT json_array_elements('${JSON.stringify(Array.isArray(data)
|
|
40
|
+
? data
|
|
41
|
+
: data[Object?.keys(data)?.[0]])
|
|
42
|
+
.replace(/'/g, "''")
|
|
43
|
+
.replace(/\\"/g, '`')}'::json) AS d )q`
|
|
44
|
+
: null;
|
|
45
|
+
const insertData = data?.features
|
|
46
|
+
? `INSERT into ${table} (geom ${','.concat(columnList1)}) SELECT public.ST_GeomFromGeoJSON((features->>'geometry')::json) AS geom ${columns1} FROM ${q1}`
|
|
47
|
+
: `INSERT into ${table} (${columnList1}) SELECT ${columns2} FROM ${q2}`;
|
|
48
|
+
return { columns, insertData };
|
|
49
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import biDatasetPost from './controllers/bi.dataset.import.js';
|
|
2
|
+
import biDatasetDemoAdd from './controllers/bi.dataset.demo.add.js';
|
|
3
|
+
|
|
4
|
+
const biSchema = {};
|
|
5
|
+
|
|
6
|
+
export default async function route(fastify, opts) {
|
|
7
|
+
fastify.route({
|
|
8
|
+
method: 'POST',
|
|
9
|
+
url: '/bi-dataset/:id',
|
|
10
|
+
schema: biSchema,
|
|
11
|
+
handler: biDatasetPost,
|
|
12
|
+
});
|
|
13
|
+
fastify.route({
|
|
14
|
+
method: 'GET',
|
|
15
|
+
url: '/bi-dataset-demo',
|
|
16
|
+
schema: biSchema,
|
|
17
|
+
handler: biDatasetDemoAdd,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { dataInsert } from "@opengis/fastify-table/utils.js";
|
|
2
|
+
|
|
3
|
+
export default async function widgetAdd({ pg, body }) {
|
|
2
4
|
try {
|
|
3
5
|
const time = Date.now();
|
|
4
6
|
const tableName = body.table_name;
|
|
@@ -7,7 +9,7 @@ export default async function widgetAdd({ pg, funcs, params = {}, body }) {
|
|
|
7
9
|
[tableName]
|
|
8
10
|
);
|
|
9
11
|
if (!checkTable.rows.length) return { message: 'bad params', status: 401 };
|
|
10
|
-
const res = await
|
|
12
|
+
const res = await dataInsert({
|
|
11
13
|
table: 'bi.dashboard',
|
|
12
14
|
data: body,
|
|
13
15
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { dataUpdate } from "@opengis/fastify-table/utils.js";
|
|
2
|
+
|
|
1
3
|
export default async function dashboardEdit(
|
|
2
|
-
{ pg,
|
|
4
|
+
{ pg, params = {}, body = {} },
|
|
3
5
|
reply
|
|
4
6
|
) {
|
|
5
7
|
try {
|
|
@@ -24,7 +26,7 @@ export default async function dashboardEdit(
|
|
|
24
26
|
.then((res1) => res1.rows?.[0] || {});
|
|
25
27
|
const { dashboard_id: dashboardId } = row;
|
|
26
28
|
|
|
27
|
-
const res = await
|
|
29
|
+
const res = await dataUpdate({
|
|
28
30
|
table: 'bi.dashboard',
|
|
29
31
|
id: dashboardId,
|
|
30
32
|
data: body,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { dataInsert, dataUpdate } from "@opengis/fastify-table/utils.js";
|
|
2
|
+
|
|
1
3
|
/* eslint-disable import/extensions */
|
|
2
4
|
import { yamlSafe } from '../../../../utils.js';
|
|
3
5
|
|
|
@@ -7,7 +9,7 @@ function generateUniqueName(prefix = 'bar') {
|
|
|
7
9
|
return `${prefix}_${randomPart}_${timestamp}`;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
export default async function widgetAdd({ pg,
|
|
12
|
+
export default async function widgetAdd({ pg, params = {}, body = {} }) {
|
|
11
13
|
const { name: dashboardName } = params;
|
|
12
14
|
if (!dashboardName) {
|
|
13
15
|
return { message: 'not enough params: id', status: 400 };
|
|
@@ -46,7 +48,7 @@ export default async function widgetAdd({ pg, funcs, params = {}, body = {} }) {
|
|
|
46
48
|
data?.metrics,
|
|
47
49
|
});
|
|
48
50
|
|
|
49
|
-
const res = await
|
|
51
|
+
const res = await dataUpdate({
|
|
50
52
|
table: 'bi.dashboard',
|
|
51
53
|
id: dashboardId,
|
|
52
54
|
data: {
|
|
@@ -58,7 +60,7 @@ export default async function widgetAdd({ pg, funcs, params = {}, body = {} }) {
|
|
|
58
60
|
});
|
|
59
61
|
const widgetData = { ...data, data, dashboard_id: dashboardId };
|
|
60
62
|
if (body?.yml) Object.assign(widgetData, { yml: body.yml });
|
|
61
|
-
await
|
|
63
|
+
await dataInsert({
|
|
62
64
|
table: 'bi.widget',
|
|
63
65
|
data: widgetData,
|
|
64
66
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import dataDelete from '@opengis/fastify-table/
|
|
2
|
-
import dataUpdate from '@opengis/fastify-table/crud/funcs/dataUpdate.js';
|
|
1
|
+
import { dataDelete, dataUpdate } from '@opengis/fastify-table/utils.js';
|
|
3
2
|
|
|
4
3
|
export default async function widgetDel({ pg = {}, params = {} }) {
|
|
5
4
|
const { widget: widgetName, name: dashboardName } = params;
|
|
@@ -2,14 +2,10 @@ import { dataUpdate } from '@opengis/fastify-table/utils.js';
|
|
|
2
2
|
|
|
3
3
|
import { yamlSafe } from '../../../../utils.js';
|
|
4
4
|
|
|
5
|
-
export default async function widgetEdit({
|
|
6
|
-
pg = {},
|
|
7
|
-
funcs = {},
|
|
8
|
-
body = {},
|
|
9
|
-
params = {},
|
|
10
|
-
}) {
|
|
5
|
+
export default async function widgetEdit({ pg, body, params, }) {
|
|
11
6
|
const { widget: widgetName, name: dashboardName } = params;
|
|
12
|
-
const data = body.yml ? yamlSafe.load(body.yml) : body;
|
|
7
|
+
const data = body.yml && !body.style ? yamlSafe.load(body.yml) : body;
|
|
8
|
+
|
|
13
9
|
const tableName =
|
|
14
10
|
data.data?.table || data?.table || data.table_name || body.table_name;
|
|
15
11
|
|
|
@@ -20,84 +16,37 @@ export default async function widgetEdit({
|
|
|
20
16
|
};
|
|
21
17
|
}
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`select a.widget_id, b.dashboard_id from bi.widget a, bi.dashboard b
|
|
19
|
+
|
|
20
|
+
const { widget_id: widgetId } = await pg.query(
|
|
21
|
+
`select a.widget_id , b.dashboard_id from bi.widget a, bi.dashboard b
|
|
27
22
|
where $2 in (a.widget_id, a.name) and $1 in (b.dashboard_id, b.name) order by 1,2`,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.then((res1) => res1.rows?.[0] || {});
|
|
31
|
-
const { widget_id: widgetId, dashboard_id: dashboardId } = row || {};
|
|
23
|
+
[dashboardName, widgetName]
|
|
24
|
+
).then((res1) => res1.rows?.[0] || {});
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// );
|
|
37
|
-
// if (!checkTable.rows.length) {
|
|
38
|
-
// return { message: 'bad params', status: 401 };
|
|
39
|
-
// }
|
|
26
|
+
if (!widgetId) {
|
|
27
|
+
return { message: `widget not found ${widgetName}`, status: 404 };
|
|
28
|
+
}
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
30
|
+
if (!tableName || !pg.pk?.[tableName]) {
|
|
31
|
+
return { message: 'bad params: table', status: 400 };
|
|
32
|
+
}
|
|
44
33
|
|
|
45
|
-
if (!widgetId) {
|
|
46
|
-
return { message: `widget not found ${widgetName}`, status: 404 };
|
|
47
|
-
}
|
|
48
34
|
|
|
49
|
-
const widgetData = { ...data, data };
|
|
50
|
-
if (body?.yml) Object.assign(widgetData, { yml: body.yml });
|
|
51
|
-
await funcs.dataUpdate({
|
|
52
|
-
table: 'bi.widget',
|
|
53
|
-
id: widgetId,
|
|
54
|
-
data: widgetData,
|
|
55
|
-
});
|
|
56
35
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
36
|
+
// const widgetData = { ...data, data };
|
|
37
|
+
const widgetData = body.yml && !body.style ? data : { style: data.style, data: { x: data.x, metrics: data.metrics }, controls: data.controls, type: data.type, title: data.title, table_name: data.table_name };
|
|
38
|
+
// console.log(widgetId, widgetData)
|
|
39
|
+
if (body?.yml) Object.assign(widgetData, { yml: body.yml });
|
|
40
|
+
const rows = await dataUpdate({
|
|
41
|
+
table: 'bi.widget',
|
|
42
|
+
id: widgetId,
|
|
43
|
+
data: widgetData,
|
|
44
|
+
});
|
|
66
45
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
bodyDashboard.panels =
|
|
71
|
-
Array.isArray(bodyDashboard.panels) && bodyDashboard.panels?.length
|
|
72
|
-
? bodyDashboard.panels?.map((panel) => {
|
|
73
|
-
if (panel.widget === widgetName) {
|
|
74
|
-
panel.col = body.col;
|
|
75
|
-
}
|
|
76
|
-
return panel;
|
|
77
|
-
})
|
|
78
|
-
: undefined;
|
|
79
|
-
bodyDashboard.widgets =
|
|
80
|
-
Array.isArray(bodyDashboard.widgets) && bodyDashboard?.widgets?.length
|
|
81
|
-
? bodyDashboard.widgets?.map((widget) => {
|
|
82
|
-
if (widget.name === widgetName) {
|
|
83
|
-
return body;
|
|
84
|
-
}
|
|
85
|
-
return widget;
|
|
86
|
-
})
|
|
87
|
-
: undefined;
|
|
46
|
+
return {
|
|
47
|
+
message: rows,
|
|
48
|
+
status: 200,
|
|
88
49
|
|
|
89
|
-
|
|
90
|
-
table: 'bi.dashboard',
|
|
91
|
-
id: dashboardId,
|
|
92
|
-
data: bodyDashboard,
|
|
93
|
-
});
|
|
50
|
+
};
|
|
94
51
|
|
|
95
|
-
return {
|
|
96
|
-
message: `Edited widget ${widgetName}`,
|
|
97
|
-
status: 200,
|
|
98
|
-
rows: res1,
|
|
99
|
-
};
|
|
100
|
-
} catch (err) {
|
|
101
|
-
return { error: err.toString(), status: 500 };
|
|
102
|
-
}
|
|
103
52
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { getFilterSQL } from '@opengis/fastify-table/utils.js';
|
|
2
|
+
|
|
1
3
|
import { getWidget } from '../../../../utils.js';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
import downloadClusterData from './utils/downloadClusterData.js';
|
|
6
|
+
|
|
7
|
+
const clusterExists = {};
|
|
8
|
+
|
|
9
|
+
export default async function cluster({ pg, query = {}, log }) {
|
|
4
10
|
const { widget, filter, dashboard, search } = query;
|
|
5
11
|
|
|
6
12
|
if (!widget) {
|
|
@@ -43,10 +49,17 @@ export default async function cluster({ pg, funcs, query = {}, log }) {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
if (!clusterTable?.name) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
Object.assign(clusterTable, {
|
|
53
|
+
name: 'bi.cluster',
|
|
54
|
+
title: 'title',
|
|
55
|
+
query: `type='${cluster}'`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (cluster && !clusterExists[cluster]) {
|
|
60
|
+
const res = await downloadClusterData({ pg, log, cluster });
|
|
61
|
+
if (res) return res;
|
|
62
|
+
clusterExists[cluster] = 1;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
if (clusterTable?.name && !pg.pk?.[clusterTable?.name]) {
|
|
@@ -56,35 +69,27 @@ export default async function cluster({ pg, funcs, query = {}, log }) {
|
|
|
56
69
|
};
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
const {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.then((res) => res.rows?.find((row) => row.table === table))
|
|
65
|
-
: {};
|
|
66
|
-
|
|
67
|
-
const { bbox } = await pg
|
|
68
|
-
.query(
|
|
69
|
-
`select st_asgeojson(box2d(geom))::json as bbox from ${table} where ${where || '1=1'}`
|
|
70
|
-
)
|
|
71
|
-
.then((res1) => res1.rows?.[0] || {});
|
|
72
|
+
const { bounds, extentStr } = await pg.query(`select count(*),
|
|
73
|
+
st_asgeojson(st_extent(geom))::json as bounds,
|
|
74
|
+
replace(regexp_replace(st_extent(geom)::box2d::text,'BOX\\(|\\)','','g'),' ',',') as "extentStr"
|
|
75
|
+
from ${table} where ${where || '1=1'}`).then((res) => res.rows?.[0] || {});
|
|
76
|
+
const extent = extentStr ? extentStr.split(',') : undefined;
|
|
72
77
|
|
|
73
78
|
// get sql
|
|
74
79
|
const { optimizedSQL } =
|
|
75
80
|
filter || search
|
|
76
|
-
? await
|
|
81
|
+
? await getFilterSQL({ pg, table, filter, search })
|
|
77
82
|
: {};
|
|
78
83
|
|
|
79
84
|
const q = `select "${cluster}" as name, sum("${metrics[0]}")::float as metric
|
|
80
85
|
from ${optimizedSQL ? `(${optimizedSQL})` : table} q
|
|
81
86
|
left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id from ${clusterTable?.name} where ${clusterTable?.title}=q."${cluster}" limit 1)b on 1=1
|
|
82
|
-
where ${where} group by ${cluster}, b.id`;
|
|
87
|
+
where ${where} group by ${cluster}, b.id order by sum("${metrics[0]}")::float desc`;
|
|
83
88
|
|
|
84
89
|
if (query.sql === '1') return q;
|
|
85
90
|
|
|
86
91
|
// auto Index
|
|
87
|
-
//
|
|
92
|
+
// autoIndex({ table, columns: (metrics || []).concat([cluster]) });
|
|
88
93
|
|
|
89
94
|
const { rows = [] } = await pg.query(q);
|
|
90
95
|
const vals = rows.map((el) => el.metric - 0).sort((a, b) => a - b);
|
|
@@ -96,7 +101,7 @@ export default async function cluster({ pg, funcs, query = {}, log }) {
|
|
|
96
101
|
vals[Math.floor(len * 0.75)],
|
|
97
102
|
vals[len - 1],
|
|
98
103
|
];
|
|
99
|
-
return { sizes, rows,
|
|
104
|
+
return { sizes, rows, bounds, extent, count: rows.length, total: rows?.reduce((acc, curr) => (curr.metric || 0) + acc, 0) };
|
|
100
105
|
} catch (err) {
|
|
101
106
|
log.error('bi/cluster', { error: err.toString(), query });
|
|
102
107
|
return { error: err.toString(), status: 500 };
|