@opengis/bi 1.0.11 → 1.0.13
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/assets/charts/bar.png +0 -0
- package/dist/assets/charts/funnel.png +0 -0
- package/dist/assets/charts/no_data.jpg +0 -0
- package/dist/assets/charts/number.png +0 -0
- package/dist/assets/charts/pie.png +0 -0
- package/dist/assets/charts/progress.png +0 -0
- package/dist/assets/charts/stat.png +0 -0
- package/dist/assets/images/bar.png +0 -0
- package/dist/assets/images/funnel.png +0 -0
- package/dist/assets/images/no_data.jpg +0 -0
- package/dist/assets/images/number.png +0 -0
- package/dist/assets/images/pie.png +0 -0
- package/dist/assets/images/progress.png +0 -0
- package/dist/assets/images/stat.png +0 -0
- package/dist/bi.js +1 -1
- package/dist/bi.umd.cjs +722 -113
- package/dist/{import-file-D6RYWvi_.js → import-file-1T7kpSzt.js} +43137 -43183
- package/dist/map-component-mixin-BLM9iEWA.js +18712 -0
- package/dist/style.css +1 -1
- package/dist/vs-calendar-WiK1hcHS.js +96 -0
- package/dist/vs-funnel-bar-CpPbYZ0_.js +92 -0
- package/dist/vs-heatmap-BG4eIROH.js +83 -0
- package/dist/vs-map-BRk6Fmks.js +66 -0
- package/dist/vs-map-cluster-Dfe9INqE.js +103 -0
- package/dist/vs-number-CJq-vi95.js +39 -0
- package/dist/{vs-text-UyIWGqQO.js → vs-text-DcrAdQ40.js} +60 -60
- package/package.json +11 -6
- package/server/migrations/bi.sql +27 -0
- package/server/plugins/hook.js +86 -0
- package/server/plugins/vite.js +7 -4
- package/server/routes/dashboard/controllers/dashboard.js +24 -4
- package/server/routes/data/controllers/data.js +48 -52
- package/server/routes/data/controllers/util/chartSQL.js +8 -7
- package/server/routes/data/controllers/util/normalizeData.js +22 -9
- package/server/routes/edit/controllers/dashboard.add.js +5 -1
- package/server/routes/edit/controllers/dashboard.edit.js +5 -1
- package/server/routes/edit/controllers/widget.add.js +23 -7
- package/server/routes/edit/controllers/widget.del.js +1 -1
- package/server/routes/edit/controllers/widget.edit.js +8 -5
- package/server/routes/map/controllers/cluster.js +75 -0
- package/server/routes/map/controllers/clusterVtile.js +143 -0
- package/server/routes/map/controllers/geojson.js +9 -23
- package/server/routes/map/controllers/map.js +57 -0
- package/server/routes/map/controllers/vtile.js +25 -32
- package/server/routes/map/index.mjs +7 -4
- package/server/utils/getWidget.js +56 -0
- package/utils.js +11 -0
- package/dist/vs-number-DKF5ptAP.js +0 -34
- package/server/templates/dashboard/erobota/bar_area.yml +0 -17
- package/server/templates/dashboard/erobota/bar_culture.yml +0 -18
- package/server/templates/dashboard/erobota/bar_grand.yml +0 -18
- package/server/templates/dashboard/erobota/count_grand.yml +0 -8
- package/server/templates/dashboard/erobota/index.yml +0 -47
- package/server/templates/dashboard/erobota/list_culture.yml +0 -12
- package/server/templates/dashboard/erobota/list_grant.yml +0 -12
- package/server/templates/dashboard/erobota/map.yml +0 -4
- package/server/templates/dashboard/erobota/pie_area.yml +0 -17
- package/server/templates/dashboard/erobota/pie_grant.yml +0 -17
- package/server/templates/dashboard/erobota/total_area.yml +0 -9
- package/server/templates/dashboard/erobota/total_grand.yml +0 -9
- package/server/templates/dashboard/sales/index.yml +0 -40
- package/server/templates/dashboard/sales/quarterly_revenue.yml +0 -19
- package/server/templates/dashboard/sales/quarterly_revenue_by_product_line.yml +0 -19
- package/server/templates/dashboard/sales/total_products_sold.yml +0 -9
- package/server/templates/dashboard/sales/total_products_sold_by_product_line.yml +0 -12
- package/server/templates/dashboard/sales/total_revenue.yml +0 -8
- package/server/templates/dashboard/sales/total_revenue_by_product_line.yml +0 -17
- package/server/templates/dashboard/sales/vehicle_sales_info.md +0 -17
- package/server/templates/dashboard/test3/index.yml +0 -29
- package/server/templates/dashboard/test3/quarterly_revenue.yml +0 -19
- package/server/templates/dashboard/test3/widget1.yml +0 -8
- package/server/templates/pt/vehicle_sales.md +0 -17
- package/server/templates/table/demo.cleaned_sales_data.table.json +0 -104
- package/server/templates/table/test.dataset.table.json +0 -16
- package/server/templates/widget/product_line.yml +0 -20
- package/server/templates/widget/test_vtile.yml +0 -7
|
@@ -1,98 +1,94 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
3
|
-
import autoIndex from '@opengis/fastify-table/pg/funcs/autoIndex.js';
|
|
4
|
-
import pgClients from '@opengis/fastify-table/pg/pgClients.js';
|
|
2
|
+
|
|
3
|
+
//import autoIndex from '@opengis/fastify-table/pg/funcs/autoIndex.js';
|
|
4
|
+
// import pgClients from '@opengis/fastify-table/pg/pgClients.js';
|
|
5
|
+
|
|
6
|
+
import { getPGAsync, autoIndex, pgClients, getSelectVal } from '@opengis/fastify-table/utils.js';
|
|
5
7
|
import chartSQL from './util/chartSQL.js';
|
|
6
8
|
import normalizeData from './util/normalizeData.js';
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
import { getTemplate } from '@opengis/fastify-table/utils.js';
|
|
10
|
+
import { getWidget } from '../../../../utils.js';
|
|
10
11
|
|
|
11
12
|
export default async function data({
|
|
12
|
-
|
|
13
|
+
funcs = {}, query = {},
|
|
13
14
|
}) {
|
|
14
15
|
const time = Date.now();
|
|
15
|
-
const {
|
|
16
|
-
const { dashboard, widget } = query;
|
|
17
|
-
|
|
18
|
-
if (!widget && !dashboard) {
|
|
19
|
-
return { message: 'not enough params: widget or dashboard required', status: 400 };
|
|
20
|
-
}
|
|
21
|
-
const dashboardData = dashboard ? await getTemplate('dashboard', dashboard) : null;
|
|
22
|
-
|
|
23
|
-
const { id, tableName } = !dashboardData && pg.pk['bi.dashboard'] && dashboard ?
|
|
24
|
-
await pg.query(`select dashboard_id as id, table_name as "tableName" from bi.dashboard where $1 in (dashboard_id, name)`, [dashboard]).then((res1) => res1.rows?.[0] || {}) : {};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (!dashboardData && dashboard && !id) return 'dashboard not found';
|
|
28
|
-
|
|
29
|
-
// return { t: 111, data: dashboardData, dashboard }
|
|
30
|
-
|
|
31
|
-
const widgetData = dashboard ? dashboardData?.find(el => el[0]?.includes(widget))?.[1] : await getTemplate('widget', widget);
|
|
16
|
+
const { dashboard, widget, filter, search, samples } = query;
|
|
32
17
|
|
|
18
|
+
const widgetData = await getWidget({ dashboard, widget });
|
|
33
19
|
|
|
34
|
-
if (
|
|
35
|
-
return widgetData
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (!id && !dashboardData && !widgetData) {
|
|
39
|
-
return { message: { root: config?.local ? dashboardDir : undefined, error: `not found`, widget, dashboard }, status: 404 };
|
|
40
|
-
}
|
|
20
|
+
if (widgetData.status) return widgetData;
|
|
41
21
|
|
|
42
|
-
const
|
|
43
|
-
const { type, text, data, controls, style, options } = widgetData || await pg.query(q, [id || dashboard, widget]).then((res1) => res1.rows?.[0] || {});
|
|
44
|
-
|
|
45
|
-
if (typeof widgetData === 'string') {
|
|
46
|
-
return { title: false, source: widgetData };
|
|
47
|
-
}
|
|
22
|
+
const { type, text, data = {}, controls, style, options } = widgetData;
|
|
48
23
|
|
|
49
|
-
|
|
50
|
-
Object.assign(data, { table: tableName });
|
|
51
|
-
}
|
|
24
|
+
const pg = data.db ? await getPGAsync(data.db) : pgClients.client
|
|
52
25
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
26
|
+
const { fields: cols } = await pg.query(`select * from ${data.table} limit 0`);
|
|
27
|
+
const columnTypes = cols.map(el => ({ name: el.name, type: pg.pgType?.[el.dataTypeID] }))
|
|
56
28
|
|
|
57
29
|
// data param
|
|
58
|
-
const { x, metric, table, where, groupby, xName } = normalizeData(
|
|
30
|
+
const { x, cls, metric, table, where, tableSQL, groupby, xName } = normalizeData(widgetData, query, columnTypes);
|
|
59
31
|
|
|
60
32
|
// auto Index
|
|
61
|
-
if (pg.pk[data.table]) {
|
|
62
|
-
autoIndex({ table: data.table, columns: [data?.time].concat([xName]).concat([groupby]).filter(el => el) }).catch(err => console.log(err))
|
|
33
|
+
if (pg.pk?.[data.table]) {
|
|
34
|
+
autoIndex({ table: data.table, pg, columns: [data?.time].concat([xName]).concat([groupby]).filter(el => el) }).catch(err => console.log(err))
|
|
63
35
|
}
|
|
64
36
|
|
|
65
37
|
// get group
|
|
66
|
-
const groupData = groupby ? await pg.query(`select ${groupby} as name ,count(*) from ${table} group by ${groupby} order by count(*) desc limit 20`).then(el => el.rows) : null;
|
|
38
|
+
const groupData = groupby ? await pg.query(`select ${groupby} as name ,count(*) from ${tableSQL || table} group by ${groupby} order by count(*) desc limit 20`).then(el => el.rows) : null;
|
|
39
|
+
|
|
40
|
+
if (query.sql === '2') return { x, metric, table, tableSQL, data, groupData };
|
|
67
41
|
|
|
68
|
-
|
|
42
|
+
const order = data.order || (type === 'listbar' ? 'metric desc' : null);
|
|
69
43
|
|
|
70
|
-
const
|
|
44
|
+
const { optimizedSQL = `select * from ${tableSQL || table}` } = filter || search ? await funcs.getFilterSQL({
|
|
45
|
+
pg, table, filter, search,
|
|
46
|
+
}) : {};
|
|
71
47
|
|
|
48
|
+
const sql = (chartSQL[type] || chartSQL['chart'])({ where, metric, table: `(${optimizedSQL})q`, x, groupData, groupby, order, samples });
|
|
72
49
|
|
|
73
50
|
if (query.sql) return sql;
|
|
74
51
|
|
|
75
52
|
if (!sql || sql?.includes('undefined')) {
|
|
76
|
-
return { message: { error: 'invalid sql', type, sql, where, metric, table
|
|
53
|
+
return { message: { error: 'invalid sql', type, sql, where, metric, table: `(${optimizedSQL})q`, x, groupData, groupby }, status: 500 };
|
|
77
54
|
}
|
|
78
55
|
|
|
79
56
|
const { rows, fields } = await pg.query(sql); // test with limit
|
|
80
57
|
|
|
81
|
-
|
|
58
|
+
if (cls) {
|
|
59
|
+
const values = rows.map((row) => row[x])?.filter((el, idx, arr) => el && arr.indexOf(el) === idx);
|
|
60
|
+
const vals = await getSelectVal({ pg, name: cls, values });
|
|
61
|
+
rows.filter((row) => row[x]).forEach((row) => {
|
|
62
|
+
Object.assign(row, { [x]: vals?.[row[x]] || row[x] });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const dimensions = fields.map(el => el.name);
|
|
82
67
|
|
|
83
68
|
const res = {
|
|
84
69
|
time: Date.now() - time,
|
|
85
70
|
dimensions,
|
|
86
71
|
|
|
87
|
-
dimensionsType: fields.map(el => pg.pgType[el.dataTypeID]),
|
|
72
|
+
dimensionsType: fields.map(el => pg.pgType?.[el.dataTypeID]),
|
|
88
73
|
type,
|
|
89
74
|
|
|
90
|
-
text: text ? text : data.text,
|
|
75
|
+
text: text ? text : (widgetData?.title || data.text),
|
|
91
76
|
//data: query.format === 'data' ? dimensions.map(el => rows.map(r => r[el])) : undefined,
|
|
92
77
|
source: query.format === 'array' ? dimensions.map(el => rows.map(r => r[el])) : rows,
|
|
93
78
|
style,
|
|
94
79
|
options,
|
|
95
80
|
controls,
|
|
81
|
+
params: {
|
|
82
|
+
x,
|
|
83
|
+
cls,
|
|
84
|
+
metric,
|
|
85
|
+
table,
|
|
86
|
+
tableSQL,
|
|
87
|
+
where,
|
|
88
|
+
groupby,
|
|
89
|
+
sql,
|
|
90
|
+
},
|
|
91
|
+
columnTypes,
|
|
96
92
|
};
|
|
97
93
|
return res;
|
|
98
94
|
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
function number({ metric, where, table }) {
|
|
2
|
-
const sql = `select ${metric} from ${table} where ${where}`
|
|
1
|
+
function number({ metric, where, table, samples }) {
|
|
2
|
+
const sql = `select ${metric} from ${table} where ${where} ${samples ? 'limit 10' : ''}`
|
|
3
3
|
return sql;
|
|
4
4
|
}
|
|
5
|
-
function table({ columns, table, where }) {
|
|
6
|
-
return `select ${columns.map(el => el.name || el)}::text from ${table} where ${where} limit 20 `
|
|
5
|
+
function table({ columns, table, where, samples }) {
|
|
6
|
+
return `select ${columns.map(el => el.name || el)}::text from ${table} where ${where} ${samples ? 'limit 10' : 'limit 20'} `
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
function chart({ metric, where, table, x, groupby, groupData }) {
|
|
9
|
+
function chart({ metric, where, table, x, groupby, groupData, order, samples }) {
|
|
10
10
|
|
|
11
|
-
const metricData = groupData?.map(el => `${metric} filter (where ${groupby}='${el.name}') as "${el.name}"`).join(',') || metric
|
|
11
|
+
const metricData = groupData?.map(el => `${metric} filter (where ${groupby}='${el.name}') as "${el.name}"`).join(',') || `${metric} as metric`
|
|
12
12
|
const sql = `select ${x}, ${metricData}
|
|
13
13
|
from ${table}
|
|
14
14
|
where ${where}
|
|
15
15
|
group by ${x}
|
|
16
|
-
order by ${x}
|
|
16
|
+
order by ${order || x}
|
|
17
|
+
${samples ? 'limit 10' : 'limit 100'}`;
|
|
17
18
|
return sql;
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -1,21 +1,34 @@
|
|
|
1
|
-
function normalizeData(data, query = {}) {
|
|
1
|
+
function normalizeData(data, query = {}, columnTypes = []) {
|
|
2
|
+
['x', 'groupby', 'granularity'].forEach(el => {
|
|
3
|
+
//console.log(el, query[el], columnTypes.find(col => col.name == query[el]))
|
|
4
|
+
if (!columnTypes.find(col => col.name == query[el])) { delete query[el]; }
|
|
5
|
+
});
|
|
2
6
|
|
|
7
|
+
if (!columnTypes.find(col => col.type === 'numeric' && col.name == query.metric)) { delete query.metric; }
|
|
3
8
|
|
|
4
|
-
const
|
|
9
|
+
const xName = query.x || (Array.isArray(data.x) ? data.x[0] : data.x);
|
|
10
|
+
const xTYpe = columnTypes.find(el => el.name == xName)?.type;
|
|
5
11
|
|
|
12
|
+
const granularity = xTYpe === 'date' || xTYpe?.includes('timestamp') ? (query.granularity || data.granularity || 'year') : null;
|
|
6
13
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const x = (granularity ? `date_trunc('${granularity}',${xName})::date` : null) || xName;
|
|
14
|
+
const x = (granularity ? `date_trunc('${granularity}',${xName})::date::text` : null) || xName;
|
|
10
15
|
|
|
11
16
|
const metrics = Array.isArray(data.metrics) ? data.metrics : [data.metrics];
|
|
12
|
-
const metric = metrics.length ? metrics?.filter(el => el).map(el => `${el.operator || 'sum'}(${el.name || el})`) : 'count(*)';
|
|
17
|
+
const metric = (query.metric ? `sum(${query.metric})` : null) || (metrics.length ? metrics?.filter(el => el).map(el => el.fx || `${el.operator || 'sum'}(${el.name || el})`) : 'count(*)');
|
|
13
18
|
|
|
14
|
-
const table = data
|
|
15
|
-
const where = data.query || 'true';
|
|
19
|
+
const { cls, table, filterCustom } = data;
|
|
16
20
|
const groupby = query.groupby || data.groupby;
|
|
17
21
|
//const orderby = query.orderby || data.orderby || 'count(*)';
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
const custom = query?.filterCustom?.split(',')
|
|
24
|
+
?.map((el) => filterCustom?.find((item) => item?.name === el)?.sql)
|
|
25
|
+
?.filter((el) => el)?.join(' and ');
|
|
26
|
+
const where = `${data.query || '1=1'} and ${custom || 'true'}`;
|
|
27
|
+
|
|
28
|
+
const tableSQL = data.tableSQL?.length
|
|
29
|
+
? `(select * from ${data?.table} t ${data.tableSQL.join(' \n ')} where ${where})q`
|
|
30
|
+
: undefined;
|
|
31
|
+
|
|
32
|
+
return { x, cls, metric, table, where, tableSQL, groupby, xName }
|
|
20
33
|
}
|
|
21
34
|
export default normalizeData;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export default async function widgetAdd({ pg, funcs, params = {}, body }) {
|
|
2
2
|
try {
|
|
3
3
|
const time = Date.now();
|
|
4
|
-
|
|
4
|
+
const tableName = body.table_name;
|
|
5
|
+
const checkTable = await pg.query(`select * from bi.dashboard where $1 in (table_name)`, [
|
|
6
|
+
tableName
|
|
7
|
+
]);
|
|
8
|
+
if (!checkTable.rows.length) return { message: 'bad params', status: 401 };
|
|
5
9
|
const res = await funcs.dataInsert({
|
|
6
10
|
table: 'bi.dashboard',
|
|
7
11
|
data: body
|
|
@@ -8,7 +8,11 @@ export default async function dashboardEdit({
|
|
|
8
8
|
status: 400,
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
const tableName = body.table_name;
|
|
12
|
+
const checkTable = await pg.query(`select * from bi.dashboard where $1 in (table_name)`, [
|
|
13
|
+
tableName
|
|
14
|
+
]);
|
|
15
|
+
if (!checkTable.rows.length) return { message: 'bad params', status: 401 };
|
|
12
16
|
const { name: dashboardName } = params;
|
|
13
17
|
const row = await pg.query(`select dashboard_id from bi.dashboard where $1 in (dashboard_id, name)`, [dashboardName]).then((res1) => res1.rows?.[0] || {});
|
|
14
18
|
const { dashboard_id: dashboardId } = row;
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
export default async function widgetAdd({
|
|
2
|
-
pg, funcs, params = {}, body = {},
|
|
3
|
-
}) {
|
|
1
|
+
export default async function widgetAdd({ pg, funcs, params = {}, body = {} }) {
|
|
4
2
|
const { name: dashboardName } = params;
|
|
5
3
|
if (!dashboardName) {
|
|
6
4
|
return { message: 'not enough params: id', status: 400 };
|
|
7
5
|
}
|
|
8
|
-
|
|
6
|
+
const tableName = body.table_name;
|
|
9
7
|
try {
|
|
10
|
-
const row = await pg
|
|
8
|
+
const row = await pg
|
|
9
|
+
.query(
|
|
10
|
+
`select dashboard_id, widgets, panels from bi.dashboard where $1 in (dashboard_id,name)`,
|
|
11
|
+
[dashboardName]
|
|
12
|
+
)
|
|
13
|
+
.then((res) => res.rows?.[0] || {});
|
|
14
|
+
|
|
15
|
+
const checkTable = await pg.query(`select * from bi.widget where $1 in (table_name)`, [
|
|
16
|
+
tableName
|
|
17
|
+
]);
|
|
18
|
+
if (!checkTable.rows.length) return { message: 'bad params', status: 401 };
|
|
19
|
+
|
|
11
20
|
const { dashboard_id: dashboardId } = row;
|
|
12
21
|
|
|
22
|
+
const generatedName = generateUniqueName(body.type);
|
|
23
|
+
body.name = generatedName;
|
|
13
24
|
const res = await funcs.dataUpdate({
|
|
14
25
|
table: 'bi.dashboard',
|
|
15
26
|
id: dashboardId,
|
|
@@ -20,14 +31,19 @@ export default async function widgetAdd({
|
|
|
20
31
|
});
|
|
21
32
|
const res2 = await funcs.dataInsert({
|
|
22
33
|
table: 'bi.widget',
|
|
23
|
-
data: { ...body, data: body, dashboard_id: dashboardId }
|
|
34
|
+
data: { ...body, data: body, dashboard_id: dashboardId }
|
|
24
35
|
});
|
|
25
36
|
return {
|
|
26
37
|
message: `Added widget to ${dashboardName}`,
|
|
27
38
|
status: 200,
|
|
28
|
-
rows: res
|
|
39
|
+
rows: res
|
|
29
40
|
};
|
|
30
41
|
} catch (err) {
|
|
31
42
|
return { error: err.toString(), status: 500 };
|
|
32
43
|
}
|
|
33
44
|
}
|
|
45
|
+
function generateUniqueName(prefix = 'bar') {
|
|
46
|
+
const randomPart = Math.floor(Math.random() * 10000);
|
|
47
|
+
const timestamp = Date.now();
|
|
48
|
+
return `${prefix}_${randomPart}_${timestamp}`;
|
|
49
|
+
}
|
|
@@ -14,7 +14,7 @@ export default async function widgetDel({ pg = {}, params = {} }) {
|
|
|
14
14
|
const row = await pg
|
|
15
15
|
.query(
|
|
16
16
|
`select a.widget_id, b.dashboard_id from bi.widget a, bi.dashboard b
|
|
17
|
-
where a.name
|
|
17
|
+
where $2 in (a.widget_id, a.name) and $1 in (b.dashboard_id, b.name) order by 1,2`,
|
|
18
18
|
[dashboardName, widgetName]
|
|
19
19
|
)
|
|
20
20
|
.then((res1) => res1.rows?.[0] || {});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dataUpdate from '@opengis/fastify-table/crud/funcs/dataUpdate.js';
|
|
2
2
|
export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, params = {} }) {
|
|
3
3
|
const { widget: widgetName, name: dashboardName } = params;
|
|
4
|
-
|
|
4
|
+
const tableName = body.table_name;
|
|
5
5
|
if (!widgetName || !dashboardName) {
|
|
6
6
|
return {
|
|
7
7
|
message: 'not enough params: dashboard and widget name',
|
|
@@ -13,12 +13,15 @@ export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, param
|
|
|
13
13
|
const row = await pg
|
|
14
14
|
.query(
|
|
15
15
|
`select a.widget_id, b.dashboard_id from bi.widget a, bi.dashboard b
|
|
16
|
-
where a.name
|
|
16
|
+
where $2 in (a.widget_id, a.name) and $1 in (b.dashboard_id, b.name) order by 1,2`,
|
|
17
17
|
[dashboardName, widgetName]
|
|
18
18
|
)
|
|
19
19
|
.then((res1) => res1.rows?.[0] || {});
|
|
20
|
-
|
|
20
|
+
const checkTable = await pg.query(`select * from bi.widget where $1 in (table_name)`, [
|
|
21
|
+
tableName
|
|
22
|
+
]);
|
|
21
23
|
const { widget_id: widgetId, dashboard_id: dashboardId } = row || {};
|
|
24
|
+
if (!checkTable.rows.length) return { message: 'bad params', status: 401 };
|
|
22
25
|
|
|
23
26
|
if (!widgetId) {
|
|
24
27
|
return { message: `widget not found ${widgetName}`, status: 404 };
|
|
@@ -47,7 +50,7 @@ export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, param
|
|
|
47
50
|
Array.isArray(bodyDashboard.panels) && bodyDashboard.panels?.length
|
|
48
51
|
? bodyDashboard.panels?.map((panel) => {
|
|
49
52
|
if (panel.widget === widgetName) {
|
|
50
|
-
panel.col = body.col;
|
|
53
|
+
panel.col = body.col;
|
|
51
54
|
}
|
|
52
55
|
return panel;
|
|
53
56
|
})
|
|
@@ -56,7 +59,7 @@ export default async function widgetEdit({ pg = {}, funcs = {}, body = {}, param
|
|
|
56
59
|
Array.isArray(bodyDashboard.widgets) && bodyDashboard?.widgets?.length
|
|
57
60
|
? bodyDashboard.widgets?.map((widget) => {
|
|
58
61
|
if (widget.name === widgetName) {
|
|
59
|
-
return body;
|
|
62
|
+
return body;
|
|
60
63
|
}
|
|
61
64
|
return widget;
|
|
62
65
|
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
|
|
2
|
+
import { getWidget } from '../../../../utils.js';
|
|
3
|
+
|
|
4
|
+
export default async function cluster({
|
|
5
|
+
pg, funcs, query = {}, log,
|
|
6
|
+
}) {
|
|
7
|
+
const {
|
|
8
|
+
widget, filter, dashboard, search,
|
|
9
|
+
} = query;
|
|
10
|
+
|
|
11
|
+
if (!widget) {
|
|
12
|
+
return { message: 'not enough params: widget', status: 400 };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { data } = await getWidget({ dashboard, widget })
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
|
|
19
|
+
const pkey = pg.pk?.[data?.table];
|
|
20
|
+
|
|
21
|
+
if (!pkey) {
|
|
22
|
+
return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`, status: 400 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// data param
|
|
26
|
+
const { table, query: where = '1=1', metrics = [], cluster, clusterTable = {} } = data;
|
|
27
|
+
|
|
28
|
+
if (!cluster) {
|
|
29
|
+
return { message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`, status: 400 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!metrics.length) {
|
|
33
|
+
return { message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`, status: 400 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!clusterTable?.name) {
|
|
37
|
+
return { message: 'not enough widget params: clusterTable name', status: 400 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (clusterTable?.name && !pg.pk?.[clusterTable?.name]) {
|
|
41
|
+
return { message: 'invalid widget params: clusterTable pkey not found', status: 404 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { total = 0 } = pg.pk?.[table]
|
|
45
|
+
? await pg.queryCache(`select oid::regclass as table, reltuples AS total from pg_class`)
|
|
46
|
+
.then((res) => res.rows?.find((row) => row.table === table))
|
|
47
|
+
: {};
|
|
48
|
+
|
|
49
|
+
const { bbox } = await pg.query(`select st_asgeojson(box2d(geom))::json as bbox from ${table} where ${where || '1=1'}`).then((res1) => res1.rows?.[0] || {});
|
|
50
|
+
|
|
51
|
+
// get sql
|
|
52
|
+
const { optimizedSQL } = filter || search
|
|
53
|
+
? await funcs.getFilterSQL({ pg, table, filter, search })
|
|
54
|
+
: {};
|
|
55
|
+
|
|
56
|
+
const q = `select "${cluster}" as name, sum("${metrics[0]}")::float as metric
|
|
57
|
+
from ${optimizedSQL ? `(${optimizedSQL})` : table} q
|
|
58
|
+
left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id from ${clusterTable?.name} where ${clusterTable?.title}=q."${cluster}" limit 1)b on 1=1
|
|
59
|
+
where ${where} group by ${cluster}, b.id`;
|
|
60
|
+
|
|
61
|
+
if (query.sql === '1') return q;
|
|
62
|
+
|
|
63
|
+
// auto Index
|
|
64
|
+
// funcs.autoIndex({ table, columns: (metrics || []).concat([cluster]) });
|
|
65
|
+
|
|
66
|
+
const { rows = [] } = await pg.query(q);
|
|
67
|
+
const vals = rows.map(el => el.metric - 0).sort((a, b) => a - b);
|
|
68
|
+
const len = vals.length;
|
|
69
|
+
const sizes = [vals[0], vals[Math.floor(len / 4)], vals[Math.floor(len / 2)], vals[Math.floor(len * 0.75)], vals[len - 1]]
|
|
70
|
+
return { sizes, rows, bbox, count: rows.length, total };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
log.error('bi/cluster', { error: err.toString(), query });
|
|
73
|
+
return { error: err.toString(), status: 500 };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import Sphericalmercator from '@mapbox/sphericalmercator';
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
6
|
+
|
|
7
|
+
import { getWidget } from '../../../../utils.js';
|
|
8
|
+
|
|
9
|
+
const mercator = new Sphericalmercator({ size: 256 });
|
|
10
|
+
|
|
11
|
+
const clusterExists = {};
|
|
12
|
+
|
|
13
|
+
export default async function clusterVtile(req, reply) {
|
|
14
|
+
const {
|
|
15
|
+
pg, funcs, params = {}, query = {}, log,
|
|
16
|
+
} = req;
|
|
17
|
+
const { z, y } = params;
|
|
18
|
+
const x = params.x?.split('.')[0] - 0;
|
|
19
|
+
|
|
20
|
+
if (!x || !y || !z) {
|
|
21
|
+
return { message: 'not enough params: xyz', status: 400 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
widget, filter, dashboard, search, clusterZoom, nocache, pointZoom,
|
|
26
|
+
} = query;
|
|
27
|
+
|
|
28
|
+
if (!widget) {
|
|
29
|
+
return { message: 'not enough params: widget', status: 400 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
const { data } = await getWidget({ dashboard, widget })
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
const headers = {
|
|
37
|
+
'Content-Type': 'application/x-protobuf',
|
|
38
|
+
'Cache-Control': nocache || query.sql
|
|
39
|
+
? 'no-cache'
|
|
40
|
+
: 'public, max-age=86400',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const hash = [pointZoom, filter].filter((el) => el).join();
|
|
44
|
+
|
|
45
|
+
const root = funcs.getFolder(req);
|
|
46
|
+
const file = path.join(root, `/map/vtile/${widget}/${hash ? `${createHash('sha1').update(hash).digest('base64')}/` : ''}${z}/${x}/${y}.mvt`);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if (!data?.table) {
|
|
52
|
+
return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table not specified`, status: 400 };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const pkey = pg.pk?.[data?.table];
|
|
56
|
+
|
|
57
|
+
if (!pkey) {
|
|
58
|
+
return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`, status: 400 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// data param
|
|
62
|
+
const { table, query: where = '1=1', metrics = [], cluster, clusterTable = {} } = data;
|
|
63
|
+
if (!clusterTable?.name) {
|
|
64
|
+
Object.assign(data, { clusterTable: { name: 'bi.cluster', title, query: `type='${data.cluster}'` } });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!clusterExists[data?.cluster]) {
|
|
68
|
+
// const res = await fetch('https://cdn.softpro.ua/data/bi/region-ua.json');
|
|
69
|
+
// if (res?.status !== 200) {
|
|
70
|
+
// }
|
|
71
|
+
// await pg.query(`insert into bi.cluster (title,type,geom) values()`);
|
|
72
|
+
// clusterExists[data?.cluster] = 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!cluster) {
|
|
76
|
+
return { message: `invalid ${widget ? 'widget' : 'dashboard'}: cluster column not specified`, status: 400 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!metrics.length) {
|
|
80
|
+
return { message: `invalid ${widget ? 'widget' : 'dashboard'}: metric columns not found`, status: 400 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// get sql
|
|
84
|
+
const { optimizedSQL } = filter || search
|
|
85
|
+
? await funcs.getFilterSQL({ pg, table, filter, search })
|
|
86
|
+
: {};
|
|
87
|
+
|
|
88
|
+
const q = `select "${cluster}" as name, sum("${metrics[0]}")::float as metric, b.*
|
|
89
|
+
from ${optimizedSQL ? `(${optimizedSQL})` : table} q
|
|
90
|
+
left join lateral (select "${pg.pk?.[clusterTable?.name]}" as id,
|
|
91
|
+
${clusterTable?.geom || 'geom'} as geom from ${clusterTable?.name}
|
|
92
|
+
where ${clusterTable?.query || '1=1'} and ${clusterTable?.title}=q."${cluster}" limit 1
|
|
93
|
+
)b on 1=1
|
|
94
|
+
where ${where} group by
|
|
95
|
+
${cluster}, b.id, b.${clusterTable?.geom || 'geom'}`;
|
|
96
|
+
|
|
97
|
+
if (query.sql === '1') return q;
|
|
98
|
+
|
|
99
|
+
const geomCol = parseInt(z, 10) < parseInt(pointZoom, 10) ? `ST_Centroid(${clusterTable?.geom || data?.geom || 'geom'})` : clusterTable?.geom || data?.geom || 'geom';
|
|
100
|
+
|
|
101
|
+
const bbox = mercator.bbox(+y, +x, +z, false/* , '900913' */);
|
|
102
|
+
const bbox2d = `'BOX(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d`;
|
|
103
|
+
|
|
104
|
+
const q1 = `SELECT ST_AsMVT(q, 'bi', 4096, 'geom','row') as tile
|
|
105
|
+
FROM (
|
|
106
|
+
SELECT
|
|
107
|
+
floor(random() * 100000 + 1)::int + row_number() over() as row,
|
|
108
|
+
|
|
109
|
+
${pg.pk?.[clusterTable?.name] ? 'id,' : ''} name, metric,
|
|
110
|
+
|
|
111
|
+
ST_AsMVTGeom(st_transform(${geomCol}, 3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
|
|
112
|
+
|
|
113
|
+
FROM (select * from (${q})q where geom && ${bbox2d}
|
|
114
|
+
|
|
115
|
+
and geom is not null and st_srid(geom) >0
|
|
116
|
+
|
|
117
|
+
and ST_GeometryType(geom) = any ('{ "ST_Polygon", "ST_MultiPolygon" }')
|
|
118
|
+
|
|
119
|
+
limit 3000)q
|
|
120
|
+
) q`;
|
|
121
|
+
|
|
122
|
+
if (query.sql === '2') return q1;
|
|
123
|
+
|
|
124
|
+
// auto Index
|
|
125
|
+
funcs.autoIndex({ table, columns: (metrics || []).concat([cluster]) });
|
|
126
|
+
|
|
127
|
+
const { rows = [] } = await pg.query(q1);
|
|
128
|
+
|
|
129
|
+
if (query.sql === '3') return rows.map((el) => el.tile);
|
|
130
|
+
|
|
131
|
+
const buffer = Buffer.concat(rows.map((el) => Buffer.from(el.tile)));
|
|
132
|
+
|
|
133
|
+
if (!nocache) {
|
|
134
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
135
|
+
await writeFile(file, buffer, 'binary');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return reply.headers(headers).send(buffer);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
log.error('bi/clusterVtile', { error: err.toString(), query, params });
|
|
141
|
+
return { error: err.toString(), status: 500 };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -8,6 +8,8 @@ import { existsSync, /*readdirSync, */ readFileSync } from 'fs';
|
|
|
8
8
|
import yaml from '../../dashboard/controllers/utils/yaml.js';
|
|
9
9
|
import normalizeData from '../../data/controllers/util/normalizeData.js';
|
|
10
10
|
|
|
11
|
+
import { getWidget } from '../../../../utils.js';
|
|
12
|
+
|
|
11
13
|
const types = { point: 'ST_Point' /* ,ST_MultiPoint */, polygon: 'ST_Polygon,ST_MultiPolygon' };
|
|
12
14
|
const hourMs = 3.6e+6;
|
|
13
15
|
|
|
@@ -19,20 +21,12 @@ export default async function geojson(req, reply) {
|
|
|
19
21
|
|
|
20
22
|
const { filter, widget, sql, type, nocache, id, dashboard, geom = 'geom', pointZoom = 0 } = query;
|
|
21
23
|
|
|
22
|
-
const { config = {} } = funcs;
|
|
23
|
-
|
|
24
24
|
if (!widget && !dashboard) {
|
|
25
25
|
return { message: 'not enough params: widget', status: 400 };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const filePath = dashboard ? path.join(widgetDir, widget || 'index' + '.yml') : path.join(widgetDir, widget + '.yml');
|
|
32
|
-
|
|
33
|
-
if (!existsSync(filePath)) {
|
|
34
|
-
return { message: { root: config?.local ? widgetDir : undefined, error: `not found`, widget }, status: 404 };
|
|
35
|
-
}
|
|
28
|
+
const data = await getWidget({ dashboard, widget });
|
|
29
|
+
if (data.status) return data;
|
|
36
30
|
|
|
37
31
|
const hash = [pointZoom, filter].filter((el) => el).join();
|
|
38
32
|
|
|
@@ -50,29 +44,21 @@ export default async function geojson(req, reply) {
|
|
|
50
44
|
}
|
|
51
45
|
|
|
52
46
|
try {
|
|
53
|
-
const fileData = readFileSync(filePath, 'utf-8');
|
|
54
|
-
const { data = {}, table_name: tableName } = filePath.includes('.yml') ? yaml.loadSafe(fileData) : {};
|
|
55
|
-
if (!data?.table && tableName) Object.assign(data, { table: tableName });
|
|
56
|
-
|
|
57
|
-
if (!data?.table) {
|
|
58
|
-
return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table not specified`, status: 400 };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
47
|
const pkey = pg.pk?.[data?.table];
|
|
62
48
|
if (!pkey) {
|
|
63
49
|
return { message: `invalid ${widget ? 'widget' : 'dashboard'}: table pk not found (${data?.table})`, status: 400 };
|
|
64
50
|
}
|
|
65
51
|
|
|
66
52
|
// data param
|
|
67
|
-
const { table, where = '1=1', xName
|
|
53
|
+
const { table, where = '1=1', xName, x } = normalizeData(data, query);
|
|
68
54
|
|
|
69
|
-
if (!xName
|
|
55
|
+
if (!xName && !x) {
|
|
70
56
|
return { message: `invalid ${widget ? 'widget' : 'dashboard'}: x axis column not specified`, status: 400 };
|
|
71
57
|
}
|
|
72
58
|
|
|
73
59
|
// get sql
|
|
74
60
|
const filterQ = filter ? await funcs.getFilterSQL({ pg, table, filter, query }) : undefined;
|
|
75
|
-
const q = `select "${pkey}", "${xName
|
|
61
|
+
const q = `select "${pkey}", "${xName || x}", /* st_asgeojson(geom)::json as */ ${geom} as geom from ${filterQ ? `(${filterQ})` : table} q where ${where}`;
|
|
76
62
|
|
|
77
63
|
if (sql === '1') return q;
|
|
78
64
|
|
|
@@ -85,7 +71,7 @@ export default async function geojson(req, reply) {
|
|
|
85
71
|
? `st_transform(${type === 'centroid' ? `st_centroid(${geom})` : geom},${query.srid})`
|
|
86
72
|
: `${type === 'centroid' || query.point || query.centroid ? `st_centroid(${geom})` : geom}`}), 6, 0)::json as geometry,
|
|
87
73
|
(select row_to_json(tc) from (select ${'' ? `${''} as status, ` : ''}
|
|
88
|
-
${xName
|
|
74
|
+
${xName ? `${xName},` : ''}
|
|
89
75
|
${data.style?.colorAttr ? `${data.style.colorAttr},` : ''}
|
|
90
76
|
${pkey} as id,(select file_path from crm.files
|
|
91
77
|
where entity_id=q.${pkey}::text and file_status <>'3' and ext in ('png','jpg') limit 1) as image
|
|
@@ -99,7 +85,7 @@ export default async function geojson(req, reply) {
|
|
|
99
85
|
if (sql === '2') return q1;
|
|
100
86
|
|
|
101
87
|
// auto Index
|
|
102
|
-
funcs.autoIndex({ table, columns: [xName
|
|
88
|
+
funcs.autoIndex({ table, columns: [xName] });
|
|
103
89
|
|
|
104
90
|
const res = await pg.query(q1).then((res) => res.rows?.[0] || {});
|
|
105
91
|
|