Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] Add stats API for maps and layers in maps plugin #383

Merged
merged 1 commit into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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