Skip to content

Commit

Permalink
Add stats API for maps and layers in maps plugin (#362) (#384)
Browse files Browse the repository at this point in the history
* Add stats API for maps and layers in maps plugin

Signed-off-by: Junqiu Lei <junqiu@amazon.com>
(cherry picked from commit ef5be8e)

Co-authored-by: Junqiu Lei <junqiu@amazon.com>
  • Loading branch information
opensearch-trigger-bot[bot] and junqiu-lei authored Apr 5, 2023
1 parent 85b6f80 commit 69fd45a
Show file tree
Hide file tree
Showing 8 changed files with 350 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Infrastructure
* Add CHANGELOG ([#342](https://github.com/opensearch-project/dashboards-maps/pull/342))
* Add stats API for maps and layers in maps plugin ([#362](https://github.com/opensearch-project/dashboards-maps/pull/362))

### Documentation

Expand Down
12 changes: 11 additions & 1 deletion common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@ export const APP_PATH = {
LANDING_PAGE_PATH: '/',
CREATE_MAP: '/create',
EDIT_MAP: '/:id',
STATS: '/stats',
};

export const APP_API = '/api/maps-dashboards';

export enum DASHBOARDS_MAPS_LAYER_NAME {
OPENSEARCH_MAP = 'OpenSearch map',
DOCUMENTS = 'Documents',
Expand All @@ -96,6 +99,11 @@ export enum DASHBOARDS_MAPS_LAYER_TYPE {
CUSTOM_MAP = 'custom_map',
}

export enum DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE {
WMS = 'wms',
TMS = 'tms',
}

export enum DASHBOARDS_MAPS_LAYER_ICON {
OPENSEARCH_MAP = 'globe',
DOCUMENTS = 'document',
Expand Down Expand Up @@ -147,7 +155,7 @@ export const LAYER_ICON_TYPE_MAP: { [key: string]: string } = {
[DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP]: 'globe',
};

//refer https://github.com/opensearch-project/i18n-plugin/blob/main/DEVELOPER_GUIDE.md#new-locale for OSD supported languages
// refer https://github.com/opensearch-project/i18n-plugin/blob/main/DEVELOPER_GUIDE.md#new-locale for OSD supported languages
export const OSD_LANGUAGES = ['en', 'es', 'fr', 'de', 'ja', 'ko', 'zh']; // all these codes are also supported in vector tiles map
export const FALLBACK_LANGUAGE = 'en';

Expand Down Expand Up @@ -183,3 +191,5 @@ export const DRAW_FILTER_POLYGON = 'Draw Polygon';
export const DRAW_FILTER_CANCEL = 'Cancel';
export const DRAW_FILTER_RECTANGLE = 'Draw Rectangle';
export const DRAW_FILTER_SPATIAL_RELATIONS = ['intersects', 'disjoint', 'within'];

export const PER_PAGE_REQUEST_NUMBER = 50;
13 changes: 7 additions & 6 deletions public/model/mapLayerType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { Filter } from '../../../../src/plugins/data/public';
import { DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE, DASHBOARDS_MAPS_LAYER_TYPE } from '../../common';

/* eslint @typescript-eslint/consistent-type-definitions: ["error", "type"] */
export type MapLayerSpecification =
Expand All @@ -25,7 +26,7 @@ export type AbstractLayerSpecification = {
};

export type OSMLayerSpecification = AbstractLayerSpecification & {
type: 'opensearch_vector_tile_map';
type: DASHBOARDS_MAPS_LAYER_TYPE.OPENSEARCH_MAP;
source: {
dataURL: string;
};
Expand All @@ -35,7 +36,7 @@ export type OSMLayerSpecification = AbstractLayerSpecification & {
};

export type DocumentLayerSpecification = AbstractLayerSpecification & {
type: 'documents';
type: DASHBOARDS_MAPS_LAYER_TYPE.DOCUMENTS;
source: {
indexPatternRefName: string;
indexPatternId: string;
Expand Down Expand Up @@ -70,19 +71,19 @@ export type DocumentLayerSpecification = AbstractLayerSpecification & {
export type CustomLayerSpecification = CustomTMSLayerSpecification | CustomWMSLayerSpecification;

export type CustomTMSLayerSpecification = AbstractLayerSpecification & {
type: 'custom_map';
type: DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP;
source: {
url: string;
customType: 'tms';
customType: DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE.TMS;
attribution: string;
};
};

export type CustomWMSLayerSpecification = AbstractLayerSpecification & {
type: 'custom_map';
type: DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP;
source: {
url: string;
customType: 'wms';
customType: DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE.WMS;
attribution: string;
layers: string;
styles: string;
Expand Down
164 changes: 164 additions & 0 deletions server/common/stats/stats_helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { SavedObjectsFindResponse } from '../../../../../src/core/server';
import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes';
import { getMapSavedObjects, getStats } from './stats_helper';

describe('getStats', () => {
const mockSavedObjects: SavedObjectsFindResponse<MapSavedObjectAttributes> = {
page: 1,
per_page: 1000,
total: 3,
saved_objects: [
{
type: 'map',
id: 'cfa702d0-cf47-11ed-9728-3b2a82d0d675',
attributes: {
title: 'test1',
description: '',
layerList:
'[{"name":"Default map","description":"","type":"opensearch_vector_tile_map","id":"2b7a9a72-e29e-45f4-9e47-93c12c6e07cb","zoomRange":[0,22],"opacity":100,"visibility":"visible","source":{"dataURL":"https://tiles.maps.opensearch.org/data/v1.json"},"style":{"styleURL":"https://tiles.maps.opensearch.org/styles/basic.json"}},{"name":"New layer 2","description":"","type":"documents","id":"6ed74651-533c-4a4b-b453-c70ed63bbc8a","zoomRange":[0,22],"opacity":70,"visibility":"visible","source":{"indexPatternRefName":"opensearch_dashboards_sample_data_logs","geoFieldType":"geo_point","geoFieldName":"geo.coordinates","documentRequestNumber":1000,"tooltipFields":[],"showTooltips":false,"displayTooltipsOnHover":true,"applyGlobalFilters":true,"indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","filters":[{"meta":{"index":"90943e30-9a47-11e8-b64d-95841ca0b247","params":{},"alias":null,"negate":false,"disabled":false},"range":{"bytes":{"gte":11,"lt":233}},"$state":{"store":"appState"}}]},"style":{"fillColor":"#f32a8a","borderColor":"#f32a8a","borderThickness":1,"markerSize":5,"label":{"enabled":false,"textByFixed":"","textByField":"","textType":"by_field","size":15,"borderWidth":0,"color":"#000000","borderColor":"#FFFFFF"}}}]',
mapState:
'{"timeRange":{"from":"now-15m","to":"now"},"query":{"query":"","language":"kuery"},"refreshInterval":{"pause":true,"value":12000}}',
version: 1,
},
references: [],
updated_at: '2023-03-30T22:12:55.966Z',
version: 'WzIxNSwxXQ==',
score: 0,
},
{
type: 'map',
id: 'b1483670-cf4b-11ed-9fa4-bbade202e9e0',
attributes: {
title: 'test2',
description: '',
layerList:
'[{"name":"Default map","description":"","type":"opensearch_vector_tile_map","id":"ea10f34e-f927-420b-8467-ee7950143dd8","zoomRange":[0,22],"opacity":100,"visibility":"visible","source":{"dataURL":"https://tiles.maps.opensearch.org/data/v1.json"},"style":{"styleURL":"https://tiles.maps.opensearch.org/styles/basic.json"}},{"name":"New layer 2","description":"","type":"documents","id":"0b231a61-e44a-4c2c-b821-82be5441925f","zoomRange":[0,22],"opacity":70,"visibility":"visible","source":{"indexPatternRefName":"opensearch_dashboards_sample_data_logs","geoFieldType":"geo_point","geoFieldName":"geo.coordinates","documentRequestNumber":1000,"tooltipFields":[],"showTooltips":false,"displayTooltipsOnHover":true,"applyGlobalFilters":true,"indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","filters":[]},"style":{"fillColor":"#622d7f","borderColor":"#622d7f","borderThickness":1,"markerSize":5,"label":{"enabled":false,"textByFixed":"","textByField":"","textType":"by_field","size":15,"borderWidth":0,"color":"#000000","borderColor":"#FFFFFF"}}},{"name":"New layer 3","description":"","type":"documents","id":"ab1a5116-ad57-40a4-832d-be10edca4976","zoomRange":[0,22],"opacity":70,"visibility":"visible","source":{"indexPatternRefName":"opensearch_dashboards_sample_data_flights","geoFieldType":"geo_point","geoFieldName":"DestLocation","documentRequestNumber":1000,"tooltipFields":[],"showTooltips":false,"displayTooltipsOnHover":true,"applyGlobalFilters":true,"indexPatternId":"d3d7af60-4c81-11e8-b3d7-01146121b73d","filters":[]},"style":{"fillColor":"#6d11fa","borderColor":"#6d11fa","borderThickness":1,"markerSize":5,"label":{"enabled":false,"textByFixed":"","textByField":"","textType":"by_field","size":15,"borderWidth":0,"color":"#000000","borderColor":"#FFFFFF"}}}]',
mapState:
'{"timeRange":{"from":"now-7d","to":"now"},"query":{"query":"","language":"kuery"},"refreshInterval":{"pause":true,"value":12000},"spatialMetaFilters":[{"type":"geo_shape","alias":"rectangle","disabled":false,"params":{"relation":"intersects","shape":{"coordinates":[[[-106.03593830559568,46.582217440485095],[-80.61568219066639,46.582217440485095],[-80.61568219066639,28.78448193045257],[-106.03593830559568,28.78448193045257],[-106.03593830559568,46.582217440485095]]],"type":"Polygon"}},"negate":false}]}',
version: 1,
},
references: [],
updated_at: '2023-03-30T22:56:11.273Z',
version: 'WzIxOSwxXQ==',
score: 0,
},
{
type: 'map',
id: '48b5ddb0-cfd7-11ed-96c2-f323ef4d8d0b',
attributes: {
title: 'test3',
description: '',
layerList:
'[{"name":"Default map","description":"","type":"opensearch_vector_tile_map","id":"bce4c650-434c-4785-8429-b9f9f2042054","zoomRange":[0,22],"opacity":100,"visibility":"visible","source":{"dataURL":"https://tiles.maps.opensearch.org/data/v1.json"},"style":{"styleURL":"https://tiles.maps.opensearch.org/styles/basic.json"}},{"name":"New layer 2","description":"","type":"documents","id":"48104a9b-72aa-4aa0-8dbf-e19ad4462dd0","zoomRange":[0,22],"opacity":70,"visibility":"visible","source":{"indexPatternRefName":"opensearch_dashboards_sample_data_logs","geoFieldType":"geo_point","geoFieldName":"geo.coordinates","documentRequestNumber":1000,"tooltipFields":[],"showTooltips":false,"displayTooltipsOnHover":true,"applyGlobalFilters":true,"indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","filters":[]},"style":{"fillColor":"#da23f2","borderColor":"#da23f2","borderThickness":1,"markerSize":5,"label":{"enabled":false,"textByFixed":"","textByField":"","textType":"by_field","size":15,"borderWidth":0,"color":"#000000","borderColor":"#FFFFFF"}}}]',
mapState:
'{"timeRange":{"from":"now-24h","to":"now"},"query":{"query":"response:404","language":"kuery"},"refreshInterval":{"pause":true,"value":12000}}',
version: 1,
},
references: [],
updated_at: '2023-03-31T15:18:26.313Z',
version: 'WzIyMCwxXQ==',
score: 0,
},
],
};

const mockEmptySavedObjects: SavedObjectsFindResponse<MapSavedObjectAttributes> = {
page: 1,
per_page: 1000,
total: 0,
saved_objects: [],
};

it('returns expected stats', () => {
const stats = getStats(mockSavedObjects);
expect(stats.maps_total).toEqual(3);
expect(stats.layers_filters_total).toEqual(1);
expect(stats.layers_total.opensearch_vector_tile_map).toEqual(3);
expect(stats.layers_total.documents).toEqual(4);
expect(stats.layers_total.wms).toEqual(0);
expect(stats.layers_total.tms).toEqual(0);
expect(stats.maps_list.length).toEqual(3);
expect(stats.maps_list[0].id).toEqual('cfa702d0-cf47-11ed-9728-3b2a82d0d675');
expect(stats.maps_list[0].layers_filters_total).toEqual(1);
expect(stats.maps_list[0].layers_total.documents).toEqual(1);
expect(stats.maps_list[0].layers_total.opensearch_vector_tile_map).toEqual(1);
});

it('returns expected stats with empty saved objects', () => {
const stats = getStats(mockEmptySavedObjects);
expect(stats.maps_total).toEqual(0);
expect(stats.layers_filters_total).toEqual(0);
expect(stats.layers_total.opensearch_vector_tile_map).toEqual(0);
expect(stats.layers_total.documents).toEqual(0);
expect(stats.layers_total.wms).toEqual(0);
expect(stats.layers_total.tms).toEqual(0);
expect(stats.maps_list.length).toEqual(0);
});
});

describe('getMapSavedObjects', () => {
const mockSavedObjectsClient = {
find: jest.fn(),
};
const perPage = 2;

it('should fetch and return all saved objects of type MAP_SAVED_OBJECT_TYPE', async () => {
// create mock SavedObjectsClientContract
mockSavedObjectsClient.find.mockResolvedValueOnce({
total: 2,
saved_objects: [
{ id: '1', attributes: { title: 'Map 1' } },
{ id: '2', attributes: { title: 'Map 2' } },
],
});

// @ts-ignore
const result = await getMapSavedObjects(mockSavedObjectsClient, perPage);

expect(result.total).toBe(2); // total should match what was returned by mockSavedObjectsClient.find()
expect(result.saved_objects).toHaveLength(2); // all saved objects should be returned
expect(result.saved_objects[0].attributes.title).toBe('Map 1'); // first saved object should have correct title
expect(result.saved_objects[1].attributes.title).toBe('Map 2'); // second saved object should have correct title
});

it('should make additional requests if there are more than perPage maps', async () => {
mockSavedObjectsClient.find
.mockResolvedValueOnce({
total: 3,
saved_objects: [
{ id: '1', attributes: { title: 'Map 1' } },
{ id: '2', attributes: { title: 'Map 2' } },
],
})
.mockResolvedValueOnce({
total: 3,
saved_objects: [{ id: '3', attributes: { title: 'Map 3' } }],
});

// @ts-ignore
const result = await getMapSavedObjects(mockSavedObjectsClient, perPage);

expect(mockSavedObjectsClient.find).toHaveBeenCalledTimes(2); // should have made two calls to find()
expect(result.saved_objects).toHaveLength(3); // all saved objects should be returned
expect(result.saved_objects[0].attributes.title).toBe('Map 1'); // first saved object should have correct title
expect(result.saved_objects[1].attributes.title).toBe('Map 2'); // second saved object should have correct title
expect(result.saved_objects[2].attributes.title).toBe('Map 3'); // second saved object should have correct title
});

it('should return empty array if no maps are found', async () => {
mockSavedObjectsClient.find.mockResolvedValueOnce({
total: 0,
saved_objects: [],
});

// @ts-ignore
const result = await getMapSavedObjects(mockSavedObjectsClient, perPage);

expect(result.saved_objects).toHaveLength(0); // should return empty array
});
});
116 changes: 116 additions & 0 deletions server/common/stats/stats_helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { SavedObjectsFindResponse } from '../../../../../src/core/server';
import { MapLayerSpecification } from '../../../public/model/mapLayerType';
import { MapSavedObjectAttributes } from '../../../common/map_saved_object_attributes';
import {
DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE,
DASHBOARDS_MAPS_LAYER_TYPE,
MAP_SAVED_OBJECT_TYPE,
} from '../../../common';
import { SavedObjectsClientContract } from '../../../../../src/core/server';

interface MapAppStats {
maps_total: number;
layers_filters_total: number;
layers_total: { [key: string]: number };
maps_list: Array<{
id: string;
layers_filters_total: number;
layers_total: { [key: string]: number };
}>;
}

export const getStats = (
mapsSavedObjects: SavedObjectsFindResponse<MapSavedObjectAttributes>
): MapAppStats => {
const totalLayersCountByType = buildLayerTypesCountObject();
let totalLayersFiltersCount = 0;
const mapsList: Array<{
id: string;
layers_filters_total: number;
layers_total: { [key: string]: number };
}> = [];
mapsSavedObjects.saved_objects.forEach((mapRes) => {
const layersCountByType = buildLayerTypesCountObject();
let layersFiltersCount = 0;
const layerList: MapLayerSpecification[] = mapRes?.attributes?.layerList
? JSON.parse(mapRes?.attributes?.layerList)
: [];
layerList.forEach((layer) => {
if (layer.type === DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP) {
layersCountByType[layer.source.customType]++;
totalLayersCountByType[layer.source.customType]++;
} else {
layersCountByType[layer.type]++;
totalLayersCountByType[layer.type]++;
}
// @ts-ignore
const layerFiltersCount = layer.source?.filters?.length ?? 0;
layersFiltersCount += layerFiltersCount;
totalLayersFiltersCount += layerFiltersCount;
});

mapsList.push({
id: mapRes?.id,
layers_filters_total: layersFiltersCount,
layers_total: {
...layersCountByType,
},
});
});

return {
maps_total: mapsSavedObjects.total,
layers_filters_total: totalLayersFiltersCount,
layers_total: {
...totalLayersCountByType,
},
maps_list: mapsList,
};
};

const buildLayerTypesCountObject = (): { [key: string]: number } => {
const layersCountByType: { [key: string]: number } = {};
Object.values(DASHBOARDS_MAPS_LAYER_TYPE).forEach((layerType) => {
if (layerType === DASHBOARDS_MAPS_LAYER_TYPE.CUSTOM_MAP) {
Object.values(DASHBOARDS_CUSTOM_MAPS_LAYER_TYPE).forEach((customLayerType) => {
layersCountByType[customLayerType] = 0;
});
} else {
layersCountByType[layerType] = 0;
}
});
return layersCountByType;
};

export const getMapSavedObjects = async (
savedObjectsClient: SavedObjectsClientContract,
perPage: number
): Promise<SavedObjectsFindResponse<MapSavedObjectAttributes>> => {
const mapsSavedObjects: SavedObjectsFindResponse<MapSavedObjectAttributes> =
await savedObjectsClient?.find({
type: MAP_SAVED_OBJECT_TYPE,
perPage,
});
// If there are more than perPage of maps, we need to make additional requests to get all maps.
if (mapsSavedObjects.total > perPage) {
const iterations = Math.ceil(mapsSavedObjects.total / perPage);
for (let i = 1; i < iterations; i++) {
const mapsSavedObjectsPage: SavedObjectsFindResponse<MapSavedObjectAttributes> =
await savedObjectsClient?.find({
type: MAP_SAVED_OBJECT_TYPE,
perPage,
page: i + 1,
});
mapsSavedObjects.saved_objects = [
...mapsSavedObjects.saved_objects,
...mapsSavedObjectsPage.saved_objects,
];
}
}
return mapsSavedObjects;
};
Loading

0 comments on commit 69fd45a

Please sign in to comment.