diff --git a/.github/workflows/dashboards-observability-test-and-build-workflow.yml b/.github/workflows/dashboards-observability-test-and-build-workflow.yml index e85ab9c9b..e052febae 100644 --- a/.github/workflows/dashboards-observability-test-and-build-workflow.yml +++ b/.github/workflows/dashboards-observability-test-and-build-workflow.yml @@ -5,8 +5,8 @@ on: [pull_request, push] env: PLUGIN_NAME: dashboards-observability - OPENSEARCH_VERSION: '2.0' - OPENSEARCH_PLUGIN_VERSION: 2.0.1.0 + OPENSEARCH_VERSION: 'main' + OPENSEARCH_PLUGIN_VERSION: 2.1.0.0 jobs: diff --git a/dashboards-observability/.cypress/integration/1_event_analytics.spec.js b/dashboards-observability/.cypress/integration/1_event_analytics.spec.js index 981b10b53..7a3a45352 100644 --- a/dashboards-observability/.cypress/integration/1_event_analytics.spec.js +++ b/dashboards-observability/.cypress/integration/1_event_analytics.spec.js @@ -35,13 +35,13 @@ describe('Has working breadcrumbs', () => { it('Redirect to correct page on breadcrumb click', () => { landOnEventExplorer(); cy.wait(delay * 3); - cy.get('.euiBreadcrumb').contains('Explorer').click(); + cy.get('.euiBreadcrumb[href="#/event_analytics/explorer"]').contains('Explorer').click(); cy.wait(delay); cy.get('[data-test-subj="searchAutocompleteTextArea"]').should('exist'); - cy.get('.euiBreadcrumb').contains('Event analytics').click(); + cy.get('.euiBreadcrumb[href="#/event_analytics"]').contains('Event analytics').click(); cy.wait(delay); cy.get('.euiTitle').contains('Event analytics').should('exist'); - cy.get('.euiBreadcrumb').contains('Observability').click(); + cy.get('.euiBreadcrumb[href="observability-dashboards#/"]').contains('Observability').click(); cy.wait(delay); cy.get('.euiTitle').contains('Event analytics').should('exist'); }); diff --git a/dashboards-observability/.cypress/integration/7_app_analytics.spec.js b/dashboards-observability/.cypress/integration/7_app_analytics.spec.js index 62db0adbe..377b2d323 100644 --- a/dashboards-observability/.cypress/integration/7_app_analytics.spec.js +++ b/dashboards-observability/.cypress/integration/7_app_analytics.spec.js @@ -251,7 +251,7 @@ describe('Viewing application', () => { it('Adds filter when Trace group name is clicked', () => { cy.get('[data-test-subj="app-analytics-overviewTab"]').click(); cy.get('[data-test-subj="dashboard-table-trace-group-name-button"]').contains('client_create_order').click(); - cy.get('.euiTableRow').should('have.length', 1); + cy.get('.euiTableRow').should('have.length', 1, { timeout: timeoutDelay }); cy.get('[data-test-subj="client_create_orderFilterBadge"]').should('exist'); cy.get('[data-test-subj="filterBadge"]').click(); cy.get('[data-test-subj="deleteFilterIcon"]').click(); @@ -560,6 +560,7 @@ describe('Application Analytics home page', () => { cy.get('.euiTableRow').first().within(($row) => { cy.get('.euiCheckbox').click(); }); + cy.wait(delay); cy.get('[data-test-subj="appAnalyticsActionsButton"]').click(); cy.get('[data-test-subj="renameApplicationContextMenuItem"]').click(); cy.get('[data-test-subj="customModalFieldText"]').clear().focus().type(newName); @@ -586,7 +587,8 @@ describe('Application Analytics home page', () => { }); cy.get('[data-test-subj="appAnalyticsActionsButton"]').click(); cy.get('[data-test-subj="deleteApplicationContextMenuItem"]').click(); - cy.get('[data-test-subj="confirmModalConfirmButton"]').click(); + cy.get('[data-test-subj="popoverModal__deleteTextInput"]').type('delete'); + cy.get('[data-test-subj="popoverModal__deleteButton"').click(); cy.wait(delay); cy.get('.euiToast').contains(`Applications successfully deleted!`); cy.get(`[data-test-subj="${newName}ApplicationLink"]`).should('not.exist'); diff --git a/dashboards-observability/common/types/app_analytics.ts b/dashboards-observability/common/types/application_analytics.ts similarity index 57% rename from dashboards-observability/common/types/app_analytics.ts rename to dashboards-observability/common/types/application_analytics.ts index c73bbb0b0..11f42c236 100644 --- a/dashboards-observability/common/types/app_analytics.ts +++ b/dashboards-observability/common/types/application_analytics.ts @@ -7,17 +7,20 @@ export interface OptionType { label: string; } -export interface ApplicationListType { - name: string; +export interface ApplicationType { id: string; - panelId: string; - composition: string[]; dateCreated: string; dateModified: string; - availability: { name: string; color: string; mainVisId: string }; + name: string; + description: string; + baseQuery: string; + servicesEntities: string[]; + traceGroups: string[]; + panelId: string; + availability: { name: string; color: string; availabilityVisId: string }; } -export interface ApplicationType { +export interface ApplicationRequestType { name: string; description: string; baseQuery: string; @@ -26,3 +29,9 @@ export interface ApplicationType { panelId: string; availabilityVisId: string; } + +export interface AvailabilityType { + name: string; + color: string; + availabilityVisId: string; +} diff --git a/dashboards-observability/opensearch_dashboards.json b/dashboards-observability/opensearch_dashboards.json index aaa8be58e..b8d9645c6 100644 --- a/dashboards-observability/opensearch_dashboards.json +++ b/dashboards-observability/opensearch_dashboards.json @@ -1,7 +1,7 @@ { "id": "observabilityDashboards", - "version": "2.0.1.0", - "opensearchDashboardsVersion": "2.0.1", + "version": "2.1.0.0", + "opensearchDashboardsVersion": "2.1.0", "server": true, "ui": true, "requiredPlugins": [ diff --git a/dashboards-observability/package.json b/dashboards-observability/package.json index b42dd1e46..1bfdf7b80 100644 --- a/dashboards-observability/package.json +++ b/dashboards-observability/package.json @@ -1,6 +1,6 @@ { "name": "observability-dashboards", - "version": "2.0.1.0", + "version": "2.1.0.0", "main": "index.ts", "license": "Apache-2.0", "scripts": { diff --git a/dashboards-observability/public/components/application_analytics/components/app_table.tsx b/dashboards-observability/public/components/application_analytics/components/app_table.tsx index 27611aa76..75fe228af 100644 --- a/dashboards-observability/public/components/application_analytics/components/app_table.tsx +++ b/dashboards-observability/public/components/application_analytics/components/app_table.tsx @@ -34,16 +34,15 @@ import { import _ from 'lodash'; import React, { ReactElement, useEffect, useState } from 'react'; import moment from 'moment'; +import { DeleteModal } from '../../common/helpers/delete_modal'; import { AppAnalyticsComponentDeps } from '../home'; import { getCustomModal } from '../../custom_panels/helpers/modal_containers'; -import { getClearModal } from '../helpers/modal_containers'; import { pageStyles, UI_DATE_FORMAT } from '../../../../common/constants/shared'; -import { ApplicationListType } from '../../../../common/types/app_analytics'; -import { AvailabilityType } from '../helpers/types'; +import { ApplicationType, AvailabilityType } from '../../../../common/types/application_analytics'; interface AppTableProps extends AppAnalyticsComponentDeps { loading: boolean; - applications: ApplicationListType[]; + applications: ApplicationType[]; fetchApplications: () => void; renameApplication: (newAppName: string, appId: string) => void; deleteApplication: (appList: string[], panelIdList: string[], toastMessage?: string) => void; @@ -66,7 +65,7 @@ export function AppTable(props: AppTableProps) { const [isModalVisible, setIsModalVisible] = useState(false); const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const [modalLayout, setModalLayout] = useState(); - const [selectedApplications, setSelectedApplications] = useState([]); + const [selectedApplications, setSelectedApplications] = useState([]); const createButtonText = 'Create application'; useEffect(() => { @@ -129,12 +128,12 @@ export function AppTable(props: AppTableProps) { const deleteApp = () => { const applicationString = `application${selectedApplications.length > 1 ? 's' : ''}`; setModalLayout( - getClearModal( - closeModal, - onDelete, - `Delete ${selectedApplications.length} ${applicationString}`, - `Are you sure you want to delete the selected ${selectedApplications.length} ${applicationString}?` - ) + ); showModal(); }; @@ -182,7 +181,7 @@ export function AppTable(props: AppTableProps) { // Add sample application, ]; - const renderAvailability = (value: AvailabilityType, record: ApplicationListType) => { + const renderAvailability = (value: AvailabilityType, record: ApplicationType) => { if (value.color === 'loading') { return ; } else if (value.name) { @@ -230,10 +229,10 @@ export function AppTable(props: AppTableProps) { name: 'Composition', sortable: false, truncateText: true, - render: (value) => ( - + render: (value, record) => ( + - {value.join(', ')} + {record.servicesEntities.concat(record.traceGroups).join(', ')} ), @@ -250,7 +249,7 @@ export function AppTable(props: AppTableProps) { sortable: true, render: (value) => {moment(value).format(UI_DATE_FORMAT)}, }, - ] as Array>; + ] as Array>; return (
diff --git a/dashboards-observability/public/components/application_analytics/components/application.tsx b/dashboards-observability/public/components/application_analytics/components/application.tsx index fb55a6ced..ba5132be7 100644 --- a/dashboards-observability/public/components/application_analytics/components/application.tsx +++ b/dashboards-observability/public/components/application_analytics/components/application.tsx @@ -57,7 +57,10 @@ import { IQueryTab } from '../../../../common/types/explorer'; import { NotificationsStart } from '../../../../../../src/core/public'; import { AppAnalyticsComponentDeps } from '../home'; import { CustomPanelView } from '../../../../public/components/custom_panels/custom_panel_view'; -import { ApplicationType } from '../../../../common/types/app_analytics'; +import { + ApplicationRequestType, + ApplicationType, +} from '../../../../common/types/application_analytics'; import { CUSTOM_PANELS_API_PREFIX } from '../../../../common/constants/custom_panels'; import { ServiceDetailFlyout } from './flyout_components/service_detail_flyout'; import { SpanDetailFlyout } from '../../../../public/components/trace_analytics/components/traces/span_detail_flyout'; @@ -83,7 +86,7 @@ interface AppDetailProps extends AppAnalyticsComponentDeps { savedObjects: SavedObjects; timestampUtils: TimestampUtils; notifications: NotificationsStart; - updateApp: (appId: string, updateAppData: Partial, type: string) => void; + updateApp: (appId: string, updateAppData: Partial, type: string) => void; setToasts: (title: string, color?: string, text?: ReactChild) => void; callback: (childfunction: () => void) => void; } @@ -109,13 +112,16 @@ export function Application(props: AppDetailProps) { callback, } = props; const [application, setApplication] = useState({ + id: '', + dateCreated: '', + dateModified: '', name: '', description: '', baseQuery: '', servicesEntities: [], traceGroups: [], panelId: '', - availabilityVisId: '', + availability: { name: '', color: '', availabilityVisId: '' }, }); const dispatch = useDispatch(); const [triggerAvailability, setTriggerAvailability] = useState(false); @@ -381,7 +387,11 @@ export function Application(props: AppDetailProps) { }; const updateAvailabilityVizId = (vizs: VisualizationType[]) => { - if (!vizs.map((viz) => viz.savedVisualizationId).includes(application.availabilityVisId)) { + if ( + !vizs + .map((viz) => viz.savedVisualizationId) + .includes(application.availability.availabilityVisId) + ) { updateApp(appId, { availabilityVisId: '' }, 'editAvailability'); } }; diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx index 569cb8609..30b78adcd 100644 --- a/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx +++ b/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx @@ -22,7 +22,7 @@ import { ServiceObject } from '../../../trace_analytics/components/common/plots/ import { ServiceMap } from '../../../trace_analytics/components/services'; import { handleServiceMapRequest } from '../../../trace_analytics/requests/services_request_handler'; import { AppAnalyticsComponentDeps } from '../../home'; -import { OptionType } from '../../../../../common/types/app_analytics'; +import { OptionType } from '../../../../../common/types/application_analytics'; import { getClearModal } from '../../helpers/modal_containers'; interface ServiceConfigProps extends AppAnalyticsComponentDeps { diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx index e7bcf1167..e0ff9a1bb 100644 --- a/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx +++ b/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx @@ -18,7 +18,7 @@ import { import DSLService from 'public/services/requests/dsl'; import React, { useEffect, useState } from 'react'; import { FilterType } from 'public/components/trace_analytics/components/common/filters/filters'; -import { OptionType } from '../../../../../common/types/app_analytics'; +import { OptionType } from '../../../../../common/types/application_analytics'; import { filtersToDsl } from '../../../trace_analytics/components/common/helper_functions'; import { handleDashboardRequest } from '../../../trace_analytics/requests/dashboard_request_handler'; import { AppAnalyticsComponentDeps } from '../../home'; diff --git a/dashboards-observability/public/components/application_analytics/components/configuration.tsx b/dashboards-observability/public/components/application_analytics/components/configuration.tsx index c8c4ce2cb..8cb8080ff 100644 --- a/dashboards-observability/public/components/application_analytics/components/configuration.tsx +++ b/dashboards-observability/public/components/application_analytics/components/configuration.tsx @@ -2,7 +2,6 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable react-hooks/exhaustive-deps */ import { EuiBreadcrumb, @@ -24,7 +23,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { ApplicationType } from 'common/types/app_analytics'; +import { ApplicationRequestType, ApplicationType } from 'common/types/application_analytics'; import { last } from 'lodash'; import React, { useState } from 'react'; @@ -34,7 +33,7 @@ interface ConfigProps { parentBreadcrumbs: EuiBreadcrumb[]; visWithAvailability: EuiSelectOption[]; switchToAvailability: () => void; - updateApp: (appId: string, updateAppData: Partial, type: string) => void; + updateApp: (appId: string, updateAppData: Partial, type: string) => void; } export const Configuration = (props: ConfigProps) => { @@ -46,7 +45,9 @@ export const Configuration = (props: ConfigProps) => { updateApp, switchToAvailability, } = props; - const [availabilityVisId, setAvailabilityVisId] = useState(application.availabilityVisId || ''); + const [availabilityVisId, setAvailabilityVisId] = useState( + application.availability.availabilityVisId || '' + ); const onAvailabilityVisChange = (event: any) => { setAvailabilityVisId(event.target.value); diff --git a/dashboards-observability/public/components/application_analytics/components/create.tsx b/dashboards-observability/public/components/application_analytics/components/create.tsx index 390aa3612..084d53b17 100644 --- a/dashboards-observability/public/components/application_analytics/components/create.tsx +++ b/dashboards-observability/public/components/application_analytics/components/create.tsx @@ -32,15 +32,19 @@ import { TraceConfig } from './config_components/trace_config'; import { ServiceConfig } from './config_components/service_config'; import { LogConfig } from './config_components/log_config'; import { PPLReferenceFlyout } from '../../../components/common/helpers'; -import { ApplicationType, OptionType } from '../../../../common/types/app_analytics'; +import { + ApplicationRequestType, + ApplicationType, + OptionType, +} from '../../../../common/types/application_analytics'; import { fetchAppById } from '../helpers/utils'; interface CreateAppProps extends AppAnalyticsComponentDeps { dslService: DSLService; pplService: PPLService; setToasts: (title: string, color?: string, text?: ReactChild) => void; - createApp: (app: ApplicationType, type: string) => void; - updateApp: (appId: string, updateAppData: Partial, type: string) => void; + createApp: (app: ApplicationRequestType, type: string) => void; + updateApp: (appId: string, updateAppData: Partial, type: string) => void; clearStorage: () => void; existingAppId: string; } @@ -70,13 +74,16 @@ export const CreateApp = (props: CreateAppProps) => { const editMode = existingAppId !== 'undefined'; const [existingApp, setExistingApp] = useState({ + id: existingAppId, + dateCreated: '', + dateModified: '', name: '', description: '', baseQuery: '', servicesEntities: [], traceGroups: [], panelId: '', - availabilityVisId: '', + availability: { name: '', color: '', availabilityVisId: '' }, }); useEffect(() => { diff --git a/dashboards-observability/public/components/application_analytics/components/flyout_components/availability_info_flyout.tsx b/dashboards-observability/public/components/application_analytics/components/flyout_components/availability_info_flyout.tsx new file mode 100644 index 000000000..86e668f41 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/flyout_components/availability_info_flyout.tsx @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiText, + EuiCodeBlock, + EuiFlyoutFooter, + EuiButton, +} from '@elastic/eui'; +import React from 'react'; + +interface AvailabilityInfoFlyoutProps { + closeFlyout: () => void; +} + +export function AvailabilityInfoFlyout(props: AvailabilityInfoFlyoutProps) { + const { closeFlyout } = props; + + return ( + + + +

Availability

+
+
+ + +

Configure availability

+ Availability is the status of your application determined by availability levels set on a + time series metric. To create an availability level, you must configure the following: +
    +
  • color: The color of the availability badge on the home page
  • +
  • name: The text in the availability badge on the home page
  • +
  • expression: Comparison operator to determine the availability
  • +
  • value: Value to use when calculating availability
  • +
+

Configuring availability

+ By default, Application analytics shows results from the last 24 hours of your data. To + see data from a different timeframe, use the date and time selector. +

Time series metric

A time series metric is any visualization that has a query that + spans over a timestamp and is a bar/line chart. You can use the PPL language to define + arbitrary conditions on your logs to create a visualization over time. +

Example

+ + {'source = | ... | ... | stats ... by span(, 1h)'} + + You can then choose Bar or Line in visualization + configurations to create a time series metric. +
+
+ + Close + +
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/helpers/types.tsx b/dashboards-observability/public/components/application_analytics/helpers/types.tsx deleted file mode 100644 index 07ac03c9d..000000000 --- a/dashboards-observability/public/components/application_analytics/helpers/types.tsx +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export interface AvailabilityType { - name: string; - color: string; - mainVisId: string; -} diff --git a/dashboards-observability/public/components/application_analytics/helpers/utils.tsx b/dashboards-observability/public/components/application_analytics/helpers/utils.tsx index d956fb06b..f212a3caf 100644 --- a/dashboards-observability/public/components/application_analytics/helpers/utils.tsx +++ b/dashboards-observability/public/components/application_analytics/helpers/utils.tsx @@ -5,7 +5,7 @@ /* eslint-disable no-console */ import { EuiDescriptionList, EuiSelectOption, EuiSpacer, EuiText } from '@elastic/eui'; -import { ApplicationListType, ApplicationType } from 'common/types/app_analytics'; +import { ApplicationType, AvailabilityType } from 'common/types/application_analytics'; import { FilterType } from 'public/components/trace_analytics/components/common/filters/filters'; import React, { Dispatch, ReactChild } from 'react'; import { batch } from 'react-redux'; @@ -36,7 +36,6 @@ import { remove as removeQueryResult, } from '../../event_analytics/redux/slices/query_result_slice'; import { addTab, removeTab } from '../../event_analytics/redux/slices/query_tab_slice'; -import { AvailabilityType } from './types'; // Name validation export const isNameValid = (name: string, existingNames: string[]) => { @@ -100,18 +99,18 @@ export const fetchAppById = async ( ) => { return http .get(`${APP_ANALYTICS_API_PREFIX}/${applicationId}`) - .then(async (res) => { - res.application.availabilityVisId = ( + .then(async (res: ApplicationType) => { + res.availability.availabilityVisId = ( await calculateAvailability( http, pplService, - res.application, - res.application.availabilityVisId, + res, + res.availability.availabilityVisId, setVisWithAvailability ) - ).mainVisId; - setApplication(res.application); - const serviceFilters = res.application.servicesEntities.map((ser: string) => { + ).availabilityVisId; + setApplication(res); + const serviceFilters = res.servicesEntities.map((ser: string) => { return { field: 'serviceName', operator: 'is', @@ -120,7 +119,7 @@ export const fetchAppById = async ( disabled: false, }; }); - const traceFilters = res.application.traceGroups.map((tra: string) => { + const traceFilters = res.traceGroups.map((tra: string) => { return { field: 'traceGroup', operator: 'is', @@ -194,11 +193,11 @@ export const fetchPanelsVizIdList = async (http: HttpSetup, appPanelId: string) export const calculateAvailability = async ( http: HttpSetup, pplService: PPLService, - application: ApplicationType | ApplicationListType, + application: ApplicationType, availabilityVisId: string, setVisWithAvailability: (visList: EuiSelectOption[]) => void ): Promise => { - let availability = { name: '', color: '', mainVisId: '' }; + let availability = { name: '', color: '', availabilityVisId: '' }; const panelId = application.panelId; if (!panelId) return availability; // Fetches saved visualizations associated to application's panel @@ -252,7 +251,7 @@ export const calculateAvailability = async ( availability = { name: '', color: 'null', - mainVisId: '', + availabilityVisId: '', }; } else { if (!availabilityFound) { @@ -263,7 +262,7 @@ export const calculateAvailability = async ( availability = { name: level.name, color: level.color, - mainVisId: visualizationId, + availabilityVisId: visualizationId, }; availabilityFound = true; } @@ -273,7 +272,7 @@ export const calculateAvailability = async ( availability = { name: level.name, color: level.color, - mainVisId: visualizationId, + availabilityVisId: visualizationId, }; availabilityFound = true; } @@ -283,7 +282,7 @@ export const calculateAvailability = async ( availability = { name: level.name, color: level.color, - mainVisId: visualizationId, + availabilityVisId: visualizationId, }; availabilityFound = true; } @@ -293,7 +292,7 @@ export const calculateAvailability = async ( availability = { name: level.name, color: level.color, - mainVisId: visualizationId, + availabilityVisId: visualizationId, }; availabilityFound = true; } @@ -303,7 +302,7 @@ export const calculateAvailability = async ( availability = { name: level.name, color: level.color, - mainVisId: visualizationId, + availabilityVisId: visualizationId, }; availabilityFound = true; } @@ -313,7 +312,7 @@ export const calculateAvailability = async ( availability = { name: level.name, color: level.color, - mainVisId: visualizationId, + availabilityVisId: visualizationId, }; availabilityFound = true; } @@ -328,7 +327,7 @@ export const calculateAvailability = async ( } setVisWithAvailability(visWithAvailability); if (!availabilityFound && visWithAvailability.length > 0) { - return { name: '', color: 'undefined', mainVisId: '' }; + return { name: '', color: 'undefined', availabilityVisId: '' }; } return availability; }; diff --git a/dashboards-observability/public/components/application_analytics/home.tsx b/dashboards-observability/public/components/application_analytics/home.tsx index 1d837d437..95ab3d978 100644 --- a/dashboards-observability/public/components/application_analytics/home.tsx +++ b/dashboards-observability/public/components/application_analytics/home.tsx @@ -24,7 +24,10 @@ import { handleIndicesExistRequest } from '../trace_analytics/requests/request_h import { ObservabilitySideBar } from '../common/side_nav'; import { NotificationsStart } from '../../../../../src/core/public'; import { APP_ANALYTICS_API_PREFIX } from '../../../common/constants/application_analytics'; -import { ApplicationListType, ApplicationType } from '../../../common/types/app_analytics'; +import { + ApplicationRequestType, + ApplicationType, +} from '../../../common/types/application_analytics'; import { calculateAvailability, fetchPanelsVizIdList, @@ -69,7 +72,7 @@ export const Home = (props: HomeProps) => { } = props; const [triggerSwitchToEvent, setTriggerSwitchToEvent] = useState(0); const dispatch = useDispatch(); - const [applicationList, setApplicationList] = useState([]); + const [applicationList, setApplicationList] = useState([]); const [toasts, setToasts] = useState([]); const [indicesExist, setIndicesExist] = useState(true); const [appConfigs, setAppConfigs] = useState([]); @@ -208,10 +211,10 @@ export const Home = (props: HomeProps) => { .get(`${APP_ANALYTICS_API_PREFIX}/`) .then(async (res) => { // Want to calculate availability going down the table - const mainVisIdStore: Record = {}; + const availabilityVisIdStore: Record = {}; for (let i = 0; i < res.data.length; i++) { - mainVisIdStore[res.data[i].id] = res.data[i].availability.mainVisId; - res.data[i].availability = { name: '', color: 'loading', mainVisId: '' }; + availabilityVisIdStore[res.data[i].id] = res.data[i].availability.availabilityVisId; + res.data[i].availability = { name: '', color: 'loading', availabilityVisId: '' }; } setApplicationList(res.data); for (let i = res.data.length - 1; i > -1; i--) { @@ -219,12 +222,12 @@ export const Home = (props: HomeProps) => { http, pplService, res.data[i], - mainVisIdStore[res.data[i].id], + availabilityVisIdStore[res.data[i].id], () => {} ); // Need to set state with new object to trigger re-render setApplicationList([ - ...res.data.filter((app: ApplicationListType) => app.id !== res.data[i].id), + ...res.data.filter((app: ApplicationType) => app.id !== res.data[i].id), res.data[i], ]); } @@ -236,7 +239,7 @@ export const Home = (props: HomeProps) => { }; // Create a new application - const createApp = (application: ApplicationType, type: string) => { + const createApp = (application: ApplicationRequestType, type: string) => { const toast = isNameValid( application.name, applicationList.map((obj) => obj.name) @@ -248,7 +251,7 @@ export const Home = (props: HomeProps) => { const requestBody = { name: application.name, - description: application.description, + description: application.description || '', baseQuery: application.baseQuery, servicesEntities: application.servicesEntities, traceGroups: application.traceGroups, @@ -308,7 +311,11 @@ export const Home = (props: HomeProps) => { }; // Update existing application - const updateApp = (appId: string, updateAppData: Partial, type: string) => { + const updateApp = ( + appId: string, + updateAppData: Partial, + type: string + ) => { const requestBody = { appId, updateBody: updateAppData, diff --git a/dashboards-observability/public/components/common/helpers/delete_modal.tsx b/dashboards-observability/public/components/common/helpers/delete_modal.tsx new file mode 100644 index 000000000..9bf80ec1f --- /dev/null +++ b/dashboards-observability/public/components/common/helpers/delete_modal.tsx @@ -0,0 +1,79 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { + EuiOverlayMask, + EuiModal, + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +export const DeleteModal = ({ + onCancel, + onConfirm, + title, + message, +}: { + onCancel: ( + event?: React.KeyboardEvent | React.MouseEvent + ) => void; + onConfirm: (event?: React.MouseEvent) => void; + title: string; + message: string; +}) => { + const [value, setValue] = useState(''); + const onChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + }; + return ( + + + + {title} + + + + {message} + The action cannot be undone. + + + + onChange(e)} + data-test-subj="popoverModal__deleteTextInput" + /> + + + + + + Cancel + onConfirm()} + color="danger" + fill + disabled={value !== 'delete'} + data-test-subj="popoverModal__deleteButton" + > + Delete + + + + + ); +}; diff --git a/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx index 305445285..a2c7ce8d6 100644 --- a/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx +++ b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx @@ -38,10 +38,11 @@ import { CUSTOM_PANELS_DOCUMENTATION_URL, } from '../../../common/constants/custom_panels'; import { UI_DATE_FORMAT } from '../../../common/constants/shared'; -import { getCustomModal, DeletePanelModal } from './helpers/modal_containers'; +import { getCustomModal } from './helpers/modal_containers'; import { CustomPanelListType } from '../../../common/types/custom_panels'; import { getSampleDataModal } from '../common/helpers/add_sample_modal'; import { pageStyles } from '../../../common/constants/shared'; +import { DeleteModal } from '../common/helpers/delete_modal'; /* * "CustomPanelTable" module, used to view all the saved panels @@ -177,7 +178,7 @@ export const CustomPanelTable = ({ const deletePanel = () => { const customPanelString = `operational panel${selectedCustomPanels.length > 1 ? 's' : ''}`; setModalLayout( - { const deletePanel = () => { setModalLayout( - { configure({ adapter: new Adapter() }); @@ -28,13 +29,13 @@ describe('Modal Container component', () => { expect(wrapper).toMatchSnapshot(); }); - it('renders DeletePanelModal component', () => { + it('renders DeleteModal component', () => { const onCancel = jest.fn(); const onConfirm = jest.fn(); const title = 'Test Title'; const message = 'Test Message'; const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/dashboards-observability/public/components/custom_panels/helpers/modal_containers.tsx b/dashboards-observability/public/components/custom_panels/helpers/modal_containers.tsx index e403d88bb..0fb80a597 100644 --- a/dashboards-observability/public/components/custom_panels/helpers/modal_containers.tsx +++ b/dashboards-observability/public/components/custom_panels/helpers/modal_containers.tsx @@ -105,61 +105,3 @@ export const getDeleteModal = ( ); }; - -export const DeletePanelModal = ({ - onCancel, - onConfirm, - title, - message, -}: { - onCancel: ( - event?: React.KeyboardEvent | React.MouseEvent - ) => void; - onConfirm: (event?: React.MouseEvent) => void; - title: string; - message: string; -}) => { - const [value, setValue] = useState(''); - const onChange = (e: React.ChangeEvent) => { - setValue(e.target.value); - }; - return ( - - - - {title} - - - - {message} - The action cannot be undone. - - - - onChange(e)} - data-test-subj="popoverModal__deleteTextInput" - /> - - - - - - Cancel - onConfirm()} - color="danger" - fill - disabled={value !== 'delete'} - data-test-subj="popoverModal__deleteButton" - > - Delete - - - - - ); -}; diff --git a/dashboards-observability/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx b/dashboards-observability/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx index cc529f862..dc70ba7fd 100644 --- a/dashboards-observability/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx +++ b/dashboards-observability/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx @@ -27,6 +27,7 @@ const ResponsiveGridLayout = WidthProvider(Responsive); * http: http core service; * chrome: chrome core service; * panelId: OpenPanel Id + * updateAvailabilityVizId: function to update application if availabilityViz is removed from panel * panelVisualizations: list of panel visualizations * setPanelVisualizations: function to set panel visualizations * editMode: boolean to check if the panel is in edit mode @@ -168,7 +169,7 @@ export const PanelGrid = (props: PanelGridProps) => { _.omit(layout, ['static', 'moved']) ); saveVisualizationLayouts(panelId, visualizationParams); - if (!_.isEmpty(updateAvailabilityVizId)) { + if (updateAvailabilityVizId) { updateAvailabilityVizId(panelVisualizations); } } diff --git a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_availability.tsx b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_availability.tsx index 1a0f9ade5..49ad1e221 100644 --- a/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_availability.tsx +++ b/dashboards-observability/public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_availability.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { EuiButton, EuiAccordion, @@ -17,8 +17,10 @@ import { EuiFieldText, EuiSelect, htmlIdGenerator, + EuiText, } from '@elastic/eui'; import { isEmpty } from 'lodash'; +import { AvailabilityInfoFlyout } from '../../../../../../application_analytics/components/flyout_components/availability_info_flyout'; import { PPL_SPAN_REGEX } from '../../../../../../../../common/constants/shared'; export interface AvailabilityUnitType { @@ -30,6 +32,8 @@ export interface AvailabilityUnitType { } export const ConfigAvailability = ({ visualizations, onConfigChange, vizState = {} }: any) => { + const [flyoutOpen, setFlyoutOpen] = useState(false); + const closeFlyout = () => setFlyoutOpen(false); const addButtonText = '+ Add availability level'; const getAvailabilityUnit = () => { return { @@ -50,6 +54,13 @@ export const ConfigAvailability = ({ visualizations, onConfigChange, vizState = { value: '≠', text: '≠' }, ]; + const availabilityAccordionButton = ( + +   Availability  + setFlyoutOpen(true)} size="m" /> + + ); + const hasSpanInApp = visualizations.data.query.finalQuery.search(PPL_SPAN_REGEX) > 0 && visualizations.data.appData.fromApp && @@ -105,7 +116,7 @@ export const ConfigAvailability = ({ visualizations, onConfigChange, vizState = + {flyoutOpen && } ); }; diff --git a/dashboards-observability/public/components/event_analytics/home/home.tsx b/dashboards-observability/public/components/event_analytics/home/home.tsx index f25aa4c32..ecd6f79c3 100644 --- a/dashboards-observability/public/components/event_analytics/home/home.tsx +++ b/dashboards-observability/public/components/event_analytics/home/home.tsx @@ -30,6 +30,7 @@ import { EuiText, EuiHorizontalRule, } from '@elastic/eui'; +import { DeleteModal } from '../../common/helpers/delete_modal'; import { Search } from '../../common/search/search'; import { RAW_QUERY, @@ -55,7 +56,6 @@ import { init as initQueryResult, selectQueryResult } from '../redux/slices/quer import { SavedQueryTable } from './saved_objects_table'; import { selectQueries } from '../redux/slices/query_slice'; import { setSelectedQueryTab } from '../redux/slices/query_tab_slice'; -import { DeletePanelModal } from '../../custom_panels/helpers/modal_containers'; import { CUSTOM_PANELS_API_PREFIX } from '../../../../common/constants/custom_panels'; import { getSampleDataModal } from '../../common/helpers/add_sample_modal'; import { parseGetSuggestions, onItemSelect } from '../../common/search/autocomplete_logic'; @@ -299,7 +299,7 @@ export const Home = (props: IHomeProps) => { const deleteHistory = () => { const customPanelString = `${selectedHistories.length > 1 ? 'histories' : 'history'}`; setModalLayout( - => { + fetchApps = async (client: ILegacyScopedClusterClient): Promise => { try { const response = await client.callAsCurrentUser('observability.getObject', { objectType: 'application', }); - return response.observabilityObjectList.map((application: any) => { - const composition: string[] = application.application.servicesEntities.concat( - application.application.traceGroups - ); - const decodedComposition = composition.map((rec) => decodeURI(rec)); + return response.observabilityObjectList.map((object: any) => { return { - name: application.application.name, - id: application.objectId, - panelId: application.application.panelId, - composition: decodedComposition, + id: object.objectId, + dateCreated: object.createdTimeMs, + dateModified: object.lastUpdatedTimeMs, + name: object.application.name, + description: object.application.description, + baseQuery: object.application.baseQuery, + servicesEntities: object.application.servicesEntities.map((rec: string) => + decodeURI(rec) + ), + traceGroups: object.application.traceGroups.map((rec: string) => decodeURI(rec)), + panelId: object.application.panelId, availability: { name: '', color: '', - mainVisId: application.application.availabilityVisId || '', + availabilityVisId: object.application.availabilityVisId || '', }, - dateModified: application.lastUpdatedTimeMs, - dateCreated: application.createdTimeMs, }; }); } catch (err: any) { @@ -38,12 +42,31 @@ export class AppAnalyticsAdaptor { }; // Fetch application by id - fetchAppById = async (client: ILegacyScopedClusterClient, appId: string): Promise => { + fetchAppById = async ( + client: ILegacyScopedClusterClient, + appId: string + ): Promise => { try { const response = await client.callAsCurrentUser('observability.getObjectById', { objectId: appId, }); - return response.observabilityObjectList[0]; + const app = response.observabilityObjectList[0]; + return { + id: appId, + dateCreated: app.createdTimeMs, + dateModified: app.lastUpdatedTimeMs, + name: app.application.name, + description: app.application.description, + baseQuery: app.application.baseQuery, + servicesEntities: app.application.servicesEntities.map((rec: string) => decodeURI(rec)), + traceGroups: app.application.traceGroups.map((rec: string) => decodeURI(rec)), + panelId: app.application.panelId, + availability: { + name: '', + color: '', + availabilityVisId: app.application.availabilityVisId || '', + }, + }; } catch (err: any) { throw new Error('Fetch Application By Id Error: ' + err); } @@ -52,22 +75,8 @@ export class AppAnalyticsAdaptor { // Create a new application createNewApp = async ( client: ILegacyScopedClusterClient, - name: string, - description: string, - baseQuery: string, - servicesEntities: string[], - traceGroups: string[], - availabilityVisId: string + appBody: Partial ) => { - const appBody = { - name, - description, - baseQuery, - servicesEntities, - traceGroups, - availabilityVisId, - }; - try { const response = await client.callAsCurrentUser('observability.createObject', { body: { @@ -102,7 +111,7 @@ export class AppAnalyticsAdaptor { updateApp = async ( client: ILegacyScopedClusterClient, appId: string, - updateAppBody: Partial + updateAppBody: Partial ) => { try { const response = await client.callAsCurrentUser('observability.updateObjectById', { diff --git a/dashboards-observability/server/routes/application_analytics/app_analytics_router.ts b/dashboards-observability/server/routes/application_analytics/app_analytics_router.ts index 21bb8f17b..930e0d5f1 100644 --- a/dashboards-observability/server/routes/application_analytics/app_analytics_router.ts +++ b/dashboards-observability/server/routes/application_analytics/app_analytics_router.ts @@ -12,7 +12,7 @@ import { ILegacyScopedClusterClient, } from '../../../../../src/core/server'; import { APP_ANALYTICS_API_PREFIX as API_PREFIX } from '../../../common/constants/application_analytics'; -import { ApplicationListType } from '../../../common/types/app_analytics'; +import { ApplicationType } from '../../../common/types/application_analytics'; import { AppAnalyticsAdaptor } from '../../../server/adaptors/application_analytics/app_analytics_adaptor'; export function registerAppAnalyticsRouter(router: IRouter) { @@ -32,7 +32,7 @@ export function registerAppAnalyticsRouter(router: IRouter) { const opensearchClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( request ); - let applicationsData: ApplicationListType[] = []; + let applicationsData: ApplicationType[] = []; try { applicationsData = await appAnalyticsBackend.fetchApps(opensearchClient); return response.ok({ @@ -111,15 +111,7 @@ export function registerAppAnalyticsRouter(router: IRouter) { ); try { - const newAppId = await appAnalyticsBackend.createNewApp( - opensearchClient, - request.body.name, - request.body.description || '', - request.body.baseQuery, - request.body.servicesEntities, - request.body.traceGroups, - request.body.availabilityVisId || '' - ); + const newAppId = await appAnalyticsBackend.createNewApp(opensearchClient, request.body); return response.ok({ body: { message: 'Application Created', diff --git a/opensearch-observability/.gitignore b/opensearch-observability/.gitignore index de6e7bf2c..74626e2b8 100644 --- a/opensearch-observability/.gitignore +++ b/opensearch-observability/.gitignore @@ -321,3 +321,5 @@ local.properties local-test .local-* + +/artifacts/ diff --git a/opensearch-observability/build.gradle b/opensearch-observability/build.gradle index ec32839e7..52d383cd4 100644 --- a/opensearch-observability/build.gradle +++ b/opensearch-observability/build.gradle @@ -10,7 +10,7 @@ import org.opensearch.gradle.testclusters.StandaloneRestIntegTestTask buildscript { ext { isSnapshot = "true" == System.getProperty("build.snapshot", "true") - opensearch_version = System.getProperty("opensearch.version", "2.0.1-SNAPSHOT") + opensearch_version = System.getProperty("opensearch.version", "2.1.0-SNAPSHOT") buildVersionQualifier = System.getProperty("build.version_qualifier", "") version_tokens = opensearch_version.tokenize('-') opensearch_build = version_tokens[0] + '.0' @@ -50,6 +50,7 @@ apply plugin: 'java' apply plugin: 'jacoco' apply plugin: 'idea' apply plugin: 'opensearch.opensearchplugin' +apply plugin: 'opensearch.pluginzip' apply plugin: 'opensearch.testclusters' apply plugin: 'io.gitlab.arturbosch.detekt' apply plugin: 'org.jetbrains.kotlin.jvm' @@ -66,6 +67,29 @@ opensearchplugin { classname "org.opensearch.observability.ObservabilityPlugin" } +publishing { + publications { + pluginZip(MavenPublication) { publication -> + pom { + name = 'opensearch-observability' + description = 'OpenSearch Observability plugin' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + name = 'OpenSearch' + url = 'https://github.com/opensearch-project/observability' + } + } + } + } + } +} + allOpen { annotation("org.opensearch.observability.util.OpenForTesting") } diff --git a/opensearch-observability/gradle/wrapper/gradle-wrapper.properties b/opensearch-observability/gradle/wrapper/gradle-wrapper.properties index 011069143..967533df8 100644 --- a/opensearch-observability/gradle/wrapper/gradle-wrapper.properties +++ b/opensearch-observability/gradle/wrapper/gradle-wrapper.properties @@ -4,7 +4,7 @@ ## #Wed Jul 29 13:30:55 PDT 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/opensearch-observability/scripts/build.sh b/opensearch-observability/scripts/build.sh new file mode 100755 index 000000000..4b2893f30 --- /dev/null +++ b/opensearch-observability/scripts/build.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# +# Copyright OpenSearch Contributors +# SPDX-License-Identifier: Apache-2.0 +# + +set -ex + +function usage() { + echo "Usage: $0 [args]" + echo "" + echo "Arguments:" + echo -e "-v VERSION\t[Required] OpenSearch version." + echo -e "-q QUALIFIER\t[Optional] Version qualifier." + echo -e "-s SNAPSHOT\t[Optional] Build a snapshot, default is 'false'." + echo -e "-p PLATFORM\t[Optional] Platform, ignored." + echo -e "-a ARCHITECTURE\t[Optional] Build architecture, ignored." + echo -e "-o OUTPUT\t[Optional] Output path, default is 'artifacts'." + echo -e "-h help" +} + +while getopts ":h:v:q:s:o:p:a:" arg; do + case $arg in + h) + usage + exit 1 + ;; + v) + VERSION=$OPTARG + ;; + q) + QUALIFIER=$OPTARG + ;; + s) + SNAPSHOT=$OPTARG + ;; + o) + OUTPUT=$OPTARG + ;; + p) + PLATFORM=$OPTARG + ;; + a) + ARCHITECTURE=$OPTARG + ;; + :) + echo "Error: -${OPTARG} requires an argument" + usage + exit 1 + ;; + ?) + echo "Invalid option: -${arg}" + exit 1 + ;; + esac +done + +if [ -z "$VERSION" ]; then + echo "Error: You must specify the OpenSearch version" + usage + exit 1 +fi + +[[ ! -z "$QUALIFIER" ]] && VERSION=$VERSION-$QUALIFIER +[[ "$SNAPSHOT" == "true" ]] && VERSION=$VERSION-SNAPSHOT +[ -z "$OUTPUT" ] && OUTPUT=artifacts + +mkdir -p $OUTPUT + +./gradlew assemble --no-daemon --refresh-dependencies -DskipTests=true -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -Dbuild.version_qualifier=$QUALIFIER + +zipPath=$(find . -path \*build/distributions/*.zip) +distributions="$(dirname "${zipPath}")" + +echo "COPY ${distributions}/*.zip" +mkdir -p $OUTPUT/plugins +cp ${distributions}/*.zip ./$OUTPUT/plugins + +./gradlew publishPluginZipPublicationToZipStagingRepository -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -Dbuild.version_qualifier=$QUALIFIER +mkdir -p $OUTPUT/maven/org/opensearch +cp -r ./build/local-staging-repo/org/opensearch/. $OUTPUT/maven/org/opensearch diff --git a/opensearch-observability/src/test/kotlin/org/opensearch/observability/PluginRestTestCase.kt b/opensearch-observability/src/test/kotlin/org/opensearch/observability/PluginRestTestCase.kt index 759674eef..c5a1d4115 100644 --- a/opensearch-observability/src/test/kotlin/org/opensearch/observability/PluginRestTestCase.kt +++ b/opensearch-observability/src/test/kotlin/org/opensearch/observability/PluginRestTestCase.kt @@ -60,7 +60,7 @@ abstract class PluginRestTestCase : OpenSearchRestTestCase() { open fun wipeAllOpenSearchIndices() { if (preserveOpenSearchIndicesAfterTest()) return val response = client().performRequest(Request("GET", "/_cat/indices?format=json&expand_wildcards=all")) - val xContentType = XContentType.fromMediaTypeOrFormat(response.entity.contentType.value) + val xContentType = XContentType.fromMediaType(response.entity.contentType.value) xContentType.xContent().createParser( NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, response.entity.content