diff --git a/common/constants/shared.ts b/common/constants/shared.ts index ffb760ffd..65bf56a7c 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -30,6 +30,7 @@ export const observabilityPluginOrder = 6000; export const UI_DATE_FORMAT = 'MM/DD/YYYY hh:mm A'; export const PPL_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'; export const PPL_INDEX_REGEX = /(search source|source|index)\s*=\s*([^|\s]+)/i; +export const PPL_CONTAINS_TIMESTAMP_REGEX = /\|\s*.*\s*[<|<=|=|>=|>]\s*timestamp\([^\)]+\)/i; // Observability plugin URI const BASE_OBSERVABILITY_URI = '/_plugins/_observability'; diff --git a/common/types/custom_panels.ts b/common/types/custom_panels.ts new file mode 100644 index 000000000..ea7d03cf4 --- /dev/null +++ b/common/types/custom_panels.ts @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export type VisualizationType = { + id: string; + title: string; + x: number; + y: number; + w: number; + h: number; + query: string; + type: string; + timeField: string; +}; + +export type PanelType = { + name: string; + visualizations: VisualizationType[]; + timeRange: { to: string; from: string }; + queryFilter: { query: string; language: string }; +}; + +export type SavedVisualizationType = { + id: string; + name: string; + query: string; + type: string; + timeField: string; +}; + +export type pplResponse = { + data: any; + metadata: any; + size: number; + status: number; +}; diff --git a/server/adaptors/custom_panels/custom_panel_adaptor.ts b/server/adaptors/custom_panels/custom_panel_adaptor.ts index 80e3a4bb2..0d173aed2 100644 --- a/server/adaptors/custom_panels/custom_panel_adaptor.ts +++ b/server/adaptors/custom_panels/custom_panel_adaptor.ts @@ -9,25 +9,31 @@ * GitHub history for details. */ -import { PanelType } from '../../../common/constants/custom_panels'; -import { ILegacyScopedClusterClient } from '../../../../../src/core/server'; +import { + PanelType, + VisualizationType, +} from "../../../common/types/custom_panels"; +import { ILegacyScopedClusterClient } from "../../../../../src/core/server"; +import { PPL_CONTAINS_TIMESTAMP_REGEX } from "../../../common/constants/shared"; -// NOTE: Need to add more functions for using panel APIs export class CustomPanelsAdaptor { // index a panel indexPanel = async function ( client: ILegacyScopedClusterClient, - body: any - ): Promise<{ panelId: string }> { + panelBody: PanelType + ): Promise<{ objectId: string }> { try { - const response = await client.callAsCurrentUser('observability.createPanel', { - body: { - panel: body, - }, - }); + const response = await client.callAsCurrentUser( + "observability.createObject", + { + body: { + operationalPanel: panelBody, + }, + } + ); return response; } catch (error) { - throw new Error('Index Panel Error:' + error); + throw new Error("Index Panel Error:" + error); } }; @@ -35,59 +41,421 @@ export class CustomPanelsAdaptor { updatePanel = async function ( client: ILegacyScopedClusterClient, panelId: string, - updateBody: Partial + updatePanelBody: Partial ) { try { - const response = await client.callAsCurrentUser('observability.updatePanelById', { - panelId: panelId, - body: { - panel: updateBody, - }, - }); + const response = await client.callAsCurrentUser( + "observability.updateObjectById", + { + objectId: panelId, + body: { + operationalPanel: updatePanelBody, + }, + } + ); return response; } catch (error) { - throw new Error('Update Panel Error:' + error); + throw new Error("Update Panel Error:" + error); } }; - //fetch a panel by id - getPanel = async function (client: ILegacyScopedClusterClient, panelId: string) { + // fetch a panel by id + getPanel = async function ( + client: ILegacyScopedClusterClient, + panelId: string + ) { try { - const response = await client.callAsCurrentUser('observability.getPanelById', { - panelId: panelId, - }); - return response.panelDetails; + const response = await client.callAsCurrentUser( + "observability.getObjectById", + { + objectId: panelId, + } + ); + return response.observabilityObjectList[0]; } catch (error) { - throw new Error('Get Panel Error:' + error); + throw new Error("Get Panel Error:" + error); } }; // gets list of panels stored in index - viewPanels = async function (client: ILegacyScopedClusterClient) { - try { - const response = await client.callAsCurrentUser('observability.getPanels'); - return response.panelsDetailsList.map((panel) => ({ - path: panel.panel.name, - id: panel.id, - dateCreated: panel.panel.dateCreated, - dateModified: panel.panel.dateModified, + viewPanelList = async function (client: ILegacyScopedClusterClient) { + try { + const response = await client.callAsCurrentUser( + "observability.getObject", + { + objectType: "operationalPanel", + } + ); + return response.observabilityObjectList.map((panel: any) => ({ + name: panel.operationalPanel.name, + id: panel.objectId, + dateCreated: panel.createdTimeMs, + dateModified: panel.lastUpdatedTimeMs, })); } catch (error) { - if (error.body.error.type === 'index_not_found_exception') { - return []; - } else throw new Error('View Panels Error:' + error); + throw new Error("View Panel List Error:" + error); } }; // Delete a panel by Id - deleteNote = async function (client: ILegacyScopedClusterClient, panelId: string) { + deletePanel = async function ( + client: ILegacyScopedClusterClient, + panelId: string + ) { + try { + const response = await client.callAsCurrentUser( + "observability.deleteObjectById", + { + objectId: panelId, + } + ); + return { status: "OK", message: response }; + } catch (error) { + throw new Error("Delete Panel Error:" + error); + } + }; + + // Delete a panel by Id + deletePanelList = async function ( + client: ILegacyScopedClusterClient, + panelIdList: string + ) { + try { + const response = await client.callAsCurrentUser( + "observability.deleteObjectByIdList", + { + objectIdList: panelIdList, + } + ); + return { status: "OK", message: response }; + } catch (error) { + throw new Error("Delete Panel List Error:" + error); + } + }; + + // Create a new Panel + createNewPanel = async ( + client: ILegacyScopedClusterClient, + panelName: string + ) => { + const panelBody = { + name: panelName, + visualizations: [], + timeRange: { + to: "now", + from: "now-1d", + }, + queryFilter: { + query: "", + language: "ppl", + }, + }; + + try { + const response = await this.indexPanel(client, panelBody); + return response.objectId; + } catch (error) { + throw new Error("Create New Panel Error:" + error); + } + }; + + // Rename an existing panel + renamePanel = async ( + client: ILegacyScopedClusterClient, + panelId: string, + panelName: string + ) => { + const updatePanelBody = { + name: panelName, + }; try { - const response = await client.callAsCurrentUser('observability.deletePanelById', { - panelId: panelId, + const response = await this.updatePanel(client, panelId, updatePanelBody); + return response.objectId; + } catch (error) { + throw new Error("Rename Panel Error:" + error); + } + }; + + // Clone an existing panel + clonePanel = async ( + client: ILegacyScopedClusterClient, + panelId: string, + panelName: string + ) => { + const updatePanelBody = { + name: panelName, + }; + try { + const getPanel = await this.getPanel(client, panelId); + const clonePanelBody = { + name: panelName, + visualizations: getPanel.operationalPanel.visualizations, + timeRange: getPanel.operationalPanel.timeRange, + queryFilter: getPanel.operationalPanel.queryFilter, + }; + const indexResponse = await this.indexPanel(client, clonePanelBody); + const getClonedPanel = await this.getPanel( + client, + indexResponse.objectId + ); + return { + clonePanelId: getClonedPanel.objectId, + dateCreated: getClonedPanel.createdTimeMs, + dateModified: getClonedPanel.lastUpdatedTimeMs, + }; + } catch (error) { + throw new Error("Clone Panel Error:" + error); + } + }; + + // Add filters to an existing panel + addPanelFilter = async ( + client: ILegacyScopedClusterClient, + panelId: string, + query: string, + language: string, + to: string, + from: string + ) => { + const updatePanelBody = { + timeRange: { + to: to, + from: from, + }, + queryFilter: { + query: query, + language: language, + }, + }; + try { + const response = await this.updatePanel(client, panelId, updatePanelBody); + return response.objectId; + } catch (error) { + throw new Error("Add Panel Filter Error:" + error); + } + }; + + // Check for time filter in query + checkTimeRangeExists = (query: string) => { + return PPL_CONTAINS_TIMESTAMP_REGEX.test(query); + }; + + // savedObjects Visualzation Query Builder + // removes time filter from query + // NOTE: this is a separate function to add more fields for future releases + savedVisualizationsQueryBuilder = (query: string) => { + return this.checkTimeRangeExists(query) + ? query.replace(PPL_CONTAINS_TIMESTAMP_REGEX, "") + : query; + }; + + // gets list of panels stored in index + viewSavedVisualiationList = async (client: ILegacyScopedClusterClient) => { + try { + const response = await client.callAsCurrentUser( + "observability.getObject", + { + objectType: "savedVisualization", + } + ); + return response.observabilityObjectList.map((visualization: any) => ({ + id: visualization.objectId, + name: visualization.savedVisualization.name, + query: this.savedVisualizationsQueryBuilder( + visualization.savedVisualization.query + ), + type: visualization.savedVisualization.type, + timeField: visualization.savedVisualization.selected_timestamp.name, + })); + } catch (error) { + throw new Error("View Saved Visualizations Error:" + error); + } + }; + + //Get All Visualizations from a Panel + //Add Visualization + getVisualizations = async ( + client: ILegacyScopedClusterClient, + panelId: string + ) => { + try { + const response = await client.callAsCurrentUser( + "observability.getObjectById", + { + objectId: panelId, + } + ); + return response.observabilityObjectList[0].operationalPanel + .visualizations; + } catch (error) { + throw new Error("Get Visualizations Error:" + error); + } + }; + + // Calculate new visualization dimensions + // New visualization always joins to the end of the panel + getNewVizDimensions = (panelVisualizations: VisualizationType[]) => { + let maxY: number = 0; + let maxYH: number = 0; + + panelVisualizations.map((panelVisualization: VisualizationType) => { + if (panelVisualization.y >= maxY) { + maxY = panelVisualization.y; + maxYH = panelVisualization.h; + } + }); + + return { x: 0, y: maxY + maxYH, w: 6, h: 4 }; + }; + + //Add Visualization in the Panel + addVisualization = async ( + client: ILegacyScopedClusterClient, + panelId: string, + newVisualization: { + id: string; + title: string; + query: string; + type: string; + timeField: string; + }, + oldVisualizationId?: string + ) => { + try { + const allPanelVisualizations = await this.getVisualizations( + client, + panelId + ); + + let newDimensions; + let visualizationsList = []; + if (oldVisualizationId === undefined) { + newDimensions = this.getNewVizDimensions(allPanelVisualizations); + visualizationsList = allPanelVisualizations; + } else { + allPanelVisualizations.map((visualization: VisualizationType) => { + if (visualization.id != oldVisualizationId) { + visualizationsList.push(visualization); + } else { + newDimensions = { + x: visualization.x, + y: visualization.y, + w: visualization.w, + h: visualization.h, + }; + } + }); + } + const newPanelVisualizations = [ + ...visualizationsList, + { ...newVisualization, ...newDimensions }, + ]; + const updatePanelResponse = await this.updatePanel(client, panelId, { + visualizations: newPanelVisualizations, + }); + return newPanelVisualizations; + } catch (error) { + throw new Error("Add/Replace Visualization Error:" + error); + } + }; + + //Add Visualization in the Panel from Event Explorer + addVisualizationFromEvents = async ( + client: ILegacyScopedClusterClient, + panelId: string, + paramVisualization: { + id: string; + title: string; + query: string; + type: string; + timeField: string; + } + ) => { + try { + const allPanelVisualizations = await this.getVisualizations( + client, + panelId + ); + const newVisualization = { + ...paramVisualization, + query: this.savedVisualizationsQueryBuilder(paramVisualization.query), + }; + const newDimensions = this.getNewVizDimensions(allPanelVisualizations); + const newPanelVisualizations = [ + ...allPanelVisualizations, + { ...newVisualization, ...newDimensions }, + ]; + const updatePanelResponse = await this.updatePanel(client, panelId, { + visualizations: newPanelVisualizations, + }); + return newPanelVisualizations; + } catch (error) { + throw new Error("Add/Replace Visualization Error:" + error); + } + }; + + //Delete a Visualization in the Panel + deleteVisualization = async ( + client: ILegacyScopedClusterClient, + panelId: string, + visualizationId: string + ) => { + try { + const allPanelVisualizations = await this.getVisualizations( + client, + panelId + ); + const filteredPanelVisualizations = allPanelVisualizations.filter( + (panelVisualization: VisualizationType) => + panelVisualization.id != visualizationId + ); + const updatePanelResponse = await this.updatePanel(client, panelId, { + visualizations: filteredPanelVisualizations, + }); + return filteredPanelVisualizations; + } catch (error) { + throw new Error("Delete Visualization Error:" + error); + } + }; + + //Edits all Visualizations in the Panel + editVisualization = async ( + client: ILegacyScopedClusterClient, + panelId: string, + visualizationParams: { + i: string; + x: number; + y: number; + w: number; + h: number; + }[] + ) => { + try { + const allPanelVisualizations = await this.getVisualizations( + client, + panelId + ); + let filteredPanelVisualizations = >[]; + + for (let i = 0; i < allPanelVisualizations.length; i++) { + for (let j = 0; j < visualizationParams.length; j++) { + if (allPanelVisualizations[i].id === visualizationParams[j].i) { + filteredPanelVisualizations.push({ + ...allPanelVisualizations[i], + x: visualizationParams[j].x, + y: visualizationParams[j].y, + w: visualizationParams[j].w, + h: visualizationParams[j].h, + }); + } + } + } + const updatePanelResponse = await this.updatePanel(client, panelId, { + visualizations: filteredPanelVisualizations, }); - return { status: 'OK', message: response }; + return filteredPanelVisualizations; } catch (error) { - throw new Error('Delete Panel Error:' + error); + throw new Error("Edit Visualizations Error:" + error); } }; } diff --git a/server/routes/custom_panels/panels_router.ts b/server/routes/custom_panels/panels_router.ts index d047f63f7..9b2afd2b0 100644 --- a/server/routes/custom_panels/panels_router.ts +++ b/server/routes/custom_panels/panels_router.ts @@ -10,16 +10,17 @@ */ import { schema } from '@osd/config-schema'; +import { CustomPanelsAdaptor } from '../../adaptors/custom_panels/custom_panel_adaptor'; import { IRouter, IOpenSearchDashboardsResponse, ResponseError, - IScopedClusterClient, + ILegacyScopedClusterClient, } from '../../../../../src/core/server'; import { CUSTOM_PANELS_API_PREFIX as API_PREFIX } from '../../../common/constants/custom_panels'; export function PanelsRouter(router: IRouter) { - // NOTE: Currently the API calls are dummy and are not connected to esclient. + const customPanelBackend = new CustomPanelsAdaptor(); // Fetch all the custom panels available router.get( { @@ -31,34 +32,19 @@ export function PanelsRouter(router: IRouter) { request, response ): Promise> => { - const panelsList = [ - { - name: 'Demo Panel 2', - id: '2FG6FWGY5', - dateCreated: '2021-07-19T21:01:14.871Z', - dateModified: '2021-07-19T21:01:14.871Z', - }, - { - name: 'Demo Panel 1', - id: 'AUJFBY234', - dateCreated: '2021-07-19T21:01:14.871Z', - dateModified: '2021-07-19T21:01:14.871Z', - }, - { - name: 'Demo Panel 3', - id: 'AUJFBY674', - dateCreated: '2021-07-19T21:01:14.871Z', - dateModified: '2021-07-19T21:01:14.871Z', - }, - ]; + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); + try { + const panelsList = await customPanelBackend.viewPanelList(opensearchNotebooksClient); return response.ok({ body: { panels: panelsList, }, }); } catch (error) { - console.log('Issue in fetching panels:', error); + console.error('Issue in fetching panel list:', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, @@ -67,8 +53,7 @@ export function PanelsRouter(router: IRouter) { } ); - // Fetch the required panel by id - // returns a panel object + // Fetch the required panel by id router.get( { path: `${API_PREFIX}/panels/{panelId}`, @@ -83,140 +68,20 @@ export function PanelsRouter(router: IRouter) { request, response ): Promise> => { - let panelObject; - if (request.params.panelId == '2FG6FWGY5') { - panelObject = { - panel: { - name: 'Demo Panel 2', - dateCreated: '2021-07-19T21:01:14.871Z', - dateModified: '2021-07-19T21:01:14.871Z', - visualizations: [ - { - id: '1', - title: 'Demo Viz 1', - x: 0, - y: 0, - w: 4, - h: 2, - query: - 'source=opensearch_dashboards_sample_data_flights | fields Carrier,Origin | where Carrier='OpenSearch-Air' | stats count() by Origin', - type: 'line', - }, - { - id: '2', - title: 'Demo Viz 2', - x: 4, - y: 0, - w: 4, - h: 2, - query: - 'source=opensearch_dashboards_sample_data_flights | fields Carrier,Origin | where Carrier='OpenSearch-Air' | stats count() by Origin', - type: 'bar', - }, - { - id: '3', - title: 'Demo Viz 3', - x: 8, - y: 0, - w: 4, - h: 2, - query: - 'source=opensearch_dashboards_sample_data_flights | fields Carrier,Origin | where Carrier='OpenSearch-Air' | stats count() by Origin', - type: 'bar', - }, - { - id: '4', - title: 'Demo Viz 4', - x: 0, - y: 2, - w: 6, - h: 2, - query: - 'source=opensearch_dashboards_sample_data_flights | fields Carrier,Origin | where Carrier='OpenSearch-Air' | stats count() by Origin', - type: 'bar', - }, - { - id: '5', - title: 'Demo Viz 5', - x: 6, - y: 2, - w: 6, - h: 2, - query: - 'source=opensearch_dashboards_sample_data_flights | fields Carrier,FlightDelayMin | stats sum(FlightDelayMin) as delays by Carrier', - type: 'bar', - }, - ], - filters: [], - timeRange: { - to: 'now', - from: 'now-1d', - }, - queryFilter: { - query: '', - language: 'ppl', - }, - refreshConfig: { - pause: true, - value: 15, - }, - }, - }; - } - if (request.params.panelId == 'AUJFBY234') { - panelObject = { - panel: { - name: 'Demo Panel 1', - dateCreated: '2021-07-19T21:01:14.871Z', - dateModified: '2021-07-19T21:01:14.871Z', - visualizations: [], - filters: [], - timeRange: { - to: 'now', - from: 'now-1d', - }, - queryFilter: { - query: '', - language: 'ppl', - }, - refreshConfig: { - pause: true, - value: 15, - }, - }, - }; - } - - if (request.params.panelId == 'AUJFBY674') { - panelObject = { - panel: { - name: 'Demo Panel 3', - dateCreated: '2021-07-19T21:01:14.871Z', - dateModified: '2021-07-19T21:01:14.871Z', - visualizations: [], - filters: [], - timeRange: { - to: 'now', - from: 'now-1d', - }, - queryFilter: { - query: 'where Carrier='OpenSearch-Air'', - language: 'ppl', - }, - refreshConfig: { - pause: true, - value: 15, - }, - }, - }; - } + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); try { + const panelObject = await customPanelBackend.getPanel( + opensearchNotebooksClient, + request.params.panelId + ); return response.ok({ body: panelObject, }); } catch (error) { - console.log('Issue in fetching panel:', error); + console.error('Issue in fetching panel:', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, @@ -225,13 +90,12 @@ export function PanelsRouter(router: IRouter) { } ); - // create a new panel - // returns new Panel Id + //Create a new panel router.post( { path: `${API_PREFIX}/panels`, validate: { - params: schema.object({ + body: schema.object({ panelName: schema.string(), }), }, @@ -241,9 +105,15 @@ export function PanelsRouter(router: IRouter) { request, response ): Promise> => { - const newPanelId = ''; + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); try { + const newPanelId = await customPanelBackend.createNewPanel( + opensearchNotebooksClient, + request.body.panelName + ); return response.ok({ body: { message: 'Panel Created', @@ -251,7 +121,7 @@ export function PanelsRouter(router: IRouter) { }, }); } catch (error) { - console.log('Issue in creating new panel', error); + console.error('Issue in creating new panel', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, @@ -260,12 +130,12 @@ export function PanelsRouter(router: IRouter) { } ); - // rename an existing panel + // rename an existing panel router.patch( { path: `${API_PREFIX}/panels/rename`, validate: { - params: schema.object({ + body: schema.object({ panelId: schema.string(), panelName: schema.string(), }), @@ -276,16 +146,23 @@ export function PanelsRouter(router: IRouter) { request, response ): Promise> => { - const newPanelId = ''; + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); try { + const responseBody = await customPanelBackend.renamePanel( + opensearchNotebooksClient, + request.body.panelId, + request.body.panelName + ); return response.ok({ body: { message: 'Panel Renamed', }, }); } catch (error) { - console.log('Issue in renaming panel', error); + console.error('Issue in renaming panel', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, @@ -294,13 +171,13 @@ export function PanelsRouter(router: IRouter) { } ); - // clones an existing panel + // clones an existing panel // returns new panel Id router.post( { path: `${API_PREFIX}/panels/clone`, validate: { - params: schema.object({ + body: schema.object({ panelId: schema.string(), panelName: schema.string(), }), @@ -311,17 +188,26 @@ export function PanelsRouter(router: IRouter) { request, response ): Promise> => { - const newPanelId = ''; + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); try { + const cloneResponse = await customPanelBackend.clonePanel( + opensearchNotebooksClient, + request.body.panelId, + request.body.panelName + ); return response.ok({ body: { message: 'Panel Cloned', - newPanelId: newPanelId, + clonePanelId: cloneResponse.clonePanelId, + dateCreated: cloneResponse.dateCreated, + dateModified: cloneResponse.dateModified, }, }); } catch (error) { - console.log('Issue in renaming panel', error); + console.error('Issue in cloning panel', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, @@ -330,7 +216,7 @@ export function PanelsRouter(router: IRouter) { } ); - // delete an existing panel + // delete an existing panel router.delete( { path: `${API_PREFIX}/panels/{panelId}`, @@ -345,16 +231,23 @@ export function PanelsRouter(router: IRouter) { request, response ): Promise> => { + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); const panelId = request.params.panelId; try { + const deleteResponse = await customPanelBackend.deletePanel( + opensearchNotebooksClient, + panelId + ); return response.noContent({ body: { message: 'Panel Deleted', }, }); } catch (error) { - console.log('Issue in deleting panel', error); + console.error('Issue in deleting panel', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, @@ -366,45 +259,12 @@ export function PanelsRouter(router: IRouter) { // replaces the ppl query filter in panel router.patch( { - path: `${API_PREFIX}/panels/query`, + path: `${API_PREFIX}/panels/filter`, validate: { - params: schema.object({ + body: schema.object({ panelId: schema.string(), query: schema.string(), language: schema.string(), - }), - }, - }, - async ( - context, - request, - response - ): Promise> => { - const panelId = request.params.panelId; - - try { - return response.ok({ - body: { - message: 'Panel PPL Filter Changed', - }, - }); - } catch (error) { - console.log('Issue in adding query filter', error); - return response.custom({ - statusCode: error.statusCode || 500, - body: error.message, - }); - } - } - ); - - // replaces the datetime filter in panel - router.patch( - { - path: `${API_PREFIX}/panels/datetime`, - validate: { - params: schema.object({ - panelId: schema.string(), to: schema.string(), from: schema.string(), }), @@ -415,16 +275,26 @@ export function PanelsRouter(router: IRouter) { request, response ): Promise> => { - const panelId = request.params.panelId; + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); try { + const panelFilterResponse = await customPanelBackend.addPanelFilter( + opensearchNotebooksClient, + request.body.panelId, + request.body.query, + request.body.language, + request.body.to, + request.body.from + ); return response.ok({ body: { - message: 'Panel DateTime Filter Changed', + message: 'Panel PPL Filter Changed', }, }); } catch (error) { - console.log('Issue in adding datetime filter', error); + console.error('Issue in adding query filter', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, diff --git a/server/routes/custom_panels/visualizations_router.ts b/server/routes/custom_panels/visualizations_router.ts index b1b41efa5..c935b9d0c 100644 --- a/server/routes/custom_panels/visualizations_router.ts +++ b/server/routes/custom_panels/visualizations_router.ts @@ -10,31 +10,63 @@ */ import { schema } from '@osd/config-schema'; +import { CustomPanelsAdaptor } from '../../adaptors/custom_panels/custom_panel_adaptor'; import { IRouter, IOpenSearchDashboardsResponse, ResponseError, - IScopedClusterClient, + ILegacyScopedClusterClient, } from '../../../../../src/core/server'; import { CUSTOM_PANELS_API_PREFIX as API_PREFIX } from '../../../common/constants/custom_panels'; export function VisualizationsRouter(router: IRouter) { + // Fetch all the savedVisualzations + const customPanelBackend = new CustomPanelsAdaptor(); + router.get( + { + path: `${API_PREFIX}/visualizations`, + validate: {}, + }, + async ( + context, + request, + response + ): Promise> => { + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); + try { + const visualizationList = await customPanelBackend.viewSavedVisualiationList( + opensearchNotebooksClient + ); + return response.ok({ + body: { + visualizations: visualizationList, + }, + }); + } catch (error) { + console.error('Issue in fetching saved visualizations:', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); + // Add a new visualization to the panel router.post( { path: `${API_PREFIX}/visualizations`, validate: { - params: schema.object({ + body: schema.object({ panelId: schema.string(), newVisualization: schema.object({ id: schema.string(), title: schema.string(), - x: schema.number(), - y: schema.number(), - w: schema.number(), - h: schema.number(), query: schema.string(), type: schema.string(), + timeField: schema.string(), }), }), }, @@ -44,17 +76,24 @@ export function VisualizationsRouter(router: IRouter) { request, response ): Promise> => { - const visualizations = {}; + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); try { + const newVisualizations = await customPanelBackend.addVisualization( + opensearchNotebooksClient, + request.body.panelId, + request.body.newVisualization + ); return response.ok({ body: { message: 'Visualization Added', - visualizations: visualizations, + visualizations: newVisualizations, }, }); } catch (error) { - console.log('Issue in adding visualization:', error); + console.error('Issue in adding visualization:', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, @@ -63,23 +102,21 @@ export function VisualizationsRouter(router: IRouter) { } ); - // Replace an existing visualization + // Add a new visualization to panel from event explorer + // NOTE: This is a separate endpoint for adding event explorer visualizations to Operational Panels + // Please use `id = 'panelViz_' + htmlIdGenerator()()` to create unique visualization Id router.post( { - path: `${API_PREFIX}/visualizations/replace`, + path: `${API_PREFIX}/visualizations/event_explorer`, validate: { - params: schema.object({ + body: schema.object({ panelId: schema.string(), - oldVisualizationId: schema.string(), newVisualization: schema.object({ id: schema.string(), title: schema.string(), - x: schema.number(), - y: schema.number(), - w: schema.number(), - h: schema.number(), query: schema.string(), type: schema.string(), + timeField: schema.string(), }), }), }, @@ -89,17 +126,24 @@ export function VisualizationsRouter(router: IRouter) { request, response ): Promise> => { - const visualizations = {}; + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); try { + const newVisualizations = await customPanelBackend.addVisualizationFromEvents( + opensearchNotebooksClient, + request.body.panelId, + request.body.newVisualization + ); return response.ok({ body: { - message: 'Visualization Replaced', - visualizations: visualizations, + message: 'Visualization Added', + visualizations: newVisualizations, }, }); } catch (error) { - console.log('Issue in replacing visualization:', error); + console.error('Issue in adding visualization:', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, @@ -108,21 +152,20 @@ export function VisualizationsRouter(router: IRouter) { } ); - // Clone an existing visualization + // Replace an existing visualization router.post( { - path: `${API_PREFIX}/visualizations/clone`, + path: `${API_PREFIX}/visualizations/replace`, validate: { - params: schema.object({ + body: schema.object({ panelId: schema.string(), - cloneVisualizattionId: schema.string(), - newVisualizationParams: schema.object({ + oldVisualizationId: schema.string(), + newVisualization: schema.object({ id: schema.string(), title: schema.string(), - x: schema.number(), - y: schema.number(), - w: schema.number(), - h: schema.number(), + query: schema.string(), + type: schema.string(), + timeField: schema.string(), }), }), }, @@ -132,17 +175,25 @@ export function VisualizationsRouter(router: IRouter) { request, response ): Promise> => { - const visualizations = {}; + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); try { + const newVisualizations = await customPanelBackend.addVisualization( + opensearchNotebooksClient, + request.body.panelId, + request.body.newVisualization, + request.body.oldVisualizationId + ); return response.ok({ body: { - message: 'Visualization Cloned', - visualizations: visualizations, + message: 'Visualization Replaced', + visualizations: newVisualizations, }, }); } catch (error) { - console.log('Issue in replacing visualization:', error); + console.error('Issue in replacing visualization:', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, @@ -167,16 +218,24 @@ export function VisualizationsRouter(router: IRouter) { request, response ): Promise> => { - const visualizations = {}; + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); try { - return response.noContent({ + const newVisualizations = await customPanelBackend.deleteVisualization( + opensearchNotebooksClient, + request.params.panelId, + request.params.visualizationId + ); + return response.ok({ body: { message: 'Visualization Deleted', + visualizations: newVisualizations, }, }); } catch (error) { - console.log('Issue in deleting visualization:', error); + console.error('Issue in deleting visualization:', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, @@ -186,15 +245,16 @@ export function VisualizationsRouter(router: IRouter) { ); // changes the position of the mentioned visualizations + // Also removes the visualiations not mentioned router.put( { - path: `${API_PREFIX}/visualizations/resize`, + path: `${API_PREFIX}/visualizations/edit`, validate: { - params: schema.object({ + body: schema.object({ panelId: schema.string(), visualizationParams: schema.arrayOf( schema.object({ - id: schema.string(), + i: schema.string(), x: schema.number(), y: schema.number(), w: schema.number(), @@ -209,17 +269,24 @@ export function VisualizationsRouter(router: IRouter) { request, response ): Promise> => { - const visualizations = {}; + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); try { + const newVisualizations = await customPanelBackend.editVisualization( + opensearchNotebooksClient, + request.body.panelId, + request.body.visualizationParams + ); return response.ok({ body: { - message: 'Visualization Resized', - visualizations: visualizations, + message: 'Visualizations Edited', + visualizations: newVisualizations, }, }); } catch (error) { - console.log('Issue in deleting visualization:', error); + console.error('Issue in Editing visualizations:', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message,