mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Co-authored-by: Nathan L Smith <nathan.smith@elastic.co>
This commit is contained in:
parent
db2c8c3b20
commit
b92ce4d7d4
26 changed files with 522 additions and 347 deletions
|
@ -148,7 +148,6 @@ export const applicationUsageSchema = {
|
|||
maps: commonSchema,
|
||||
ml: commonSchema,
|
||||
monitoring: commonSchema,
|
||||
observabilityCases: commonSchema,
|
||||
'observability-overview': commonSchema,
|
||||
osquery: commonSchema,
|
||||
security_account: commonSchema,
|
||||
|
|
|
@ -3970,137 +3970,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"observabilityCases": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The application being tracked"
|
||||
}
|
||||
},
|
||||
"viewId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "Always `main`"
|
||||
}
|
||||
},
|
||||
"clicks_total": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application since we started counting them"
|
||||
}
|
||||
},
|
||||
"clicks_7_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application over the last 7 days"
|
||||
}
|
||||
},
|
||||
"clicks_30_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application over the last 30 days"
|
||||
}
|
||||
},
|
||||
"clicks_90_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application over the last 90 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_total": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen since we started counting them."
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_7_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen over the last 7 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_30_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen over the last 30 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_90_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen over the last 90 days"
|
||||
}
|
||||
},
|
||||
"views": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The application being tracked"
|
||||
}
|
||||
},
|
||||
"viewId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The application view being tracked"
|
||||
}
|
||||
},
|
||||
"clicks_total": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application sub view since we started counting them"
|
||||
}
|
||||
},
|
||||
"clicks_7_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the active application sub view over the last 7 days"
|
||||
}
|
||||
},
|
||||
"clicks_30_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the active application sub view over the last 30 days"
|
||||
}
|
||||
},
|
||||
"clicks_90_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the active application sub view over the last 90 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_total": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application sub view is active and on-screen since we started counting them."
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_7_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_30_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_90_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"observability-overview": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiSettingsParams } from '../../../../src/core/types';
|
||||
import { observabilityFeatureId } from '../../observability/common';
|
||||
import { enableServiceOverview } from '../common/ui_settings_keys';
|
||||
|
||||
/**
|
||||
|
@ -15,7 +16,7 @@ import { enableServiceOverview } from '../common/ui_settings_keys';
|
|||
*/
|
||||
export const uiSettings: Record<string, UiSettingsParams<boolean>> = {
|
||||
[enableServiceOverview]: {
|
||||
category: ['observability'],
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.apm.enableServiceOverviewExperimentName', {
|
||||
defaultMessage: 'APM Service overview',
|
||||
}),
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const CASES_APP_ID = 'observabilityCases';
|
||||
export const OBSERVABILITY = 'observability';
|
16
x-pack/plugins/observability/common/index.ts
Normal file
16
x-pack/plugins/observability/common/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const casesFeatureId = 'observabilityCases';
|
||||
|
||||
// The ID of the observability app. Should more appropriately be called
|
||||
// 'observability' but it's used in telemetry by applicationUsage so we don't
|
||||
// want to change it.
|
||||
export const observabilityAppId = 'observability-overview';
|
||||
|
||||
// Used by feature and "solution" registration
|
||||
export const observabilityFeatureId = 'observability';
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { observabilityFeatureId } from '..';
|
||||
|
||||
export const observabilityRuleRegistrySettings = {
|
||||
name: 'observability',
|
||||
name: observabilityFeatureId,
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { observabilityAppId } from '../../../../../common';
|
||||
|
||||
import {
|
||||
getCaseDetailsUrl,
|
||||
|
@ -14,7 +15,7 @@ import {
|
|||
useFormatUrl,
|
||||
} from '../../../../pages/cases/links';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { CASES_APP_ID, CASES_OWNER } from '../constants';
|
||||
import { CASES_OWNER } from '../constants';
|
||||
|
||||
export interface AllCasesNavProps {
|
||||
detailName: string;
|
||||
|
@ -30,9 +31,9 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => {
|
|||
cases: casesUi,
|
||||
application: { getUrlForApp, navigateToUrl },
|
||||
} = useKibana().services;
|
||||
const { formatUrl } = useFormatUrl(CASES_APP_ID);
|
||||
const { formatUrl } = useFormatUrl();
|
||||
|
||||
const casesUrl = getUrlForApp(CASES_APP_ID);
|
||||
const casesUrl = `${getUrlForApp(observabilityAppId)}/cases`;
|
||||
return casesUi.getAllCases({
|
||||
caseDetailsNavigation: {
|
||||
href: ({ detailName, subCaseId }: AllCasesNavProps) => {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_rea
|
|||
import { useMessagesStorage } from '../../../../hooks/use_messages_storage';
|
||||
import { createCalloutId } from './helpers';
|
||||
import { CaseCallOut, CaseCallOutProps } from '.';
|
||||
import { observabilityAppId } from '../../../../../common';
|
||||
|
||||
jest.mock('../../../../hooks/use_messages_storage');
|
||||
const useSecurityLocalStorageMock = useMessagesStorage as jest.Mock;
|
||||
|
@ -110,9 +111,9 @@ describe('CaseCallOut ', () => {
|
|||
);
|
||||
|
||||
const id = createCalloutId(['message-one']);
|
||||
expect(securityLocalStorageMock.getMessages).toHaveBeenCalledWith('observability');
|
||||
expect(securityLocalStorageMock.getMessages).toHaveBeenCalledWith(observabilityAppId);
|
||||
wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click');
|
||||
expect(securityLocalStorageMock.addMessage).toHaveBeenCalledWith('observability', id);
|
||||
expect(securityLocalStorageMock.addMessage).toHaveBeenCalledWith(observabilityAppId, id);
|
||||
});
|
||||
|
||||
it('do not show the callout if is in the localStorage', () => {
|
||||
|
|
|
@ -12,7 +12,7 @@ import { CallOut } from './callout';
|
|||
import { ErrorMessage } from './types';
|
||||
import { createCalloutId } from './helpers';
|
||||
import { useMessagesStorage } from '../../../../hooks/use_messages_storage';
|
||||
import { OBSERVABILITY } from '../../../../../common/const';
|
||||
import { observabilityAppId } from '../../../../../common';
|
||||
|
||||
export * from './helpers';
|
||||
|
||||
|
@ -35,7 +35,7 @@ interface CalloutVisibility {
|
|||
function CaseCallOutComponent({ title, messages = [] }: CaseCallOutProps) {
|
||||
const { getMessages, addMessage } = useMessagesStorage();
|
||||
|
||||
const caseMessages = useMemo(() => getMessages(OBSERVABILITY), [getMessages]);
|
||||
const caseMessages = useMemo(() => getMessages(observabilityAppId), [getMessages]);
|
||||
const dismissedCallouts = useMemo(
|
||||
() =>
|
||||
caseMessages.reduce<CalloutVisibility>(
|
||||
|
@ -53,7 +53,7 @@ function CaseCallOutComponent({ title, messages = [] }: CaseCallOutProps) {
|
|||
(id, type) => {
|
||||
setCalloutVisibility((prevState) => ({ ...prevState, [id]: false }));
|
||||
if (type === 'primary') {
|
||||
addMessage(OBSERVABILITY, id);
|
||||
addMessage(observabilityAppId, id);
|
||||
}
|
||||
},
|
||||
[setCalloutVisibility, addMessage]
|
||||
|
|
|
@ -17,8 +17,8 @@ import {
|
|||
import { Case } from '../../../../../../cases/common';
|
||||
import { useFetchAlertData } from './helpers';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { CASES_APP_ID } from '../constants';
|
||||
import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs';
|
||||
import { observabilityAppId } from '../../../../../common';
|
||||
|
||||
interface Props {
|
||||
caseId: string;
|
||||
|
@ -47,7 +47,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
|
|||
application: { getUrlForApp, navigateToUrl },
|
||||
} = useKibana().services;
|
||||
const allCasesLink = getCaseUrl();
|
||||
const { formatUrl } = useFormatUrl(CASES_APP_ID);
|
||||
const { formatUrl } = useFormatUrl();
|
||||
const href = formatUrl(allCasesLink);
|
||||
useBreadcrumbs([
|
||||
{ ...casesBreadcrumbs.cases, href },
|
||||
|
@ -81,7 +81,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
|
|||
[caseId, formatUrl, subCaseId]
|
||||
);
|
||||
|
||||
const casesUrl = getUrlForApp(CASES_APP_ID);
|
||||
const casesUrl = `${getUrlForApp(observabilityAppId)}/cases`;
|
||||
return casesUi.getCaseView({
|
||||
allCasesNavigation: {
|
||||
href: allCasesHref,
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CASES_APP_ID } from '../../../../common/const';
|
||||
import { observabilityFeatureId } from '../../../../common';
|
||||
|
||||
export { CASES_APP_ID };
|
||||
export const CASES_OWNER = 'observability';
|
||||
export const CASES_OWNER = observabilityFeatureId;
|
||||
|
|
|
@ -21,7 +21,7 @@ jest.mock('../../../../utils/kibana_react');
|
|||
describe('Create case', () => {
|
||||
const mockCreateCase = jest.fn();
|
||||
const mockNavigateToUrl = jest.fn();
|
||||
const mockCasesUrl = 'https://elastic.co/app/observability/cases';
|
||||
const mockObservabilityUrl = 'https://elastic.co/app/observability';
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
|
@ -29,7 +29,7 @@ describe('Create case', () => {
|
|||
cases: {
|
||||
getCreateCase: mockCreateCase,
|
||||
},
|
||||
application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl },
|
||||
application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockObservabilityUrl },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -53,7 +53,7 @@ describe('Create case', () => {
|
|||
onCancel();
|
||||
},
|
||||
},
|
||||
application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl },
|
||||
application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockObservabilityUrl },
|
||||
},
|
||||
});
|
||||
mount(
|
||||
|
@ -62,7 +62,9 @@ describe('Create case', () => {
|
|||
</EuiThemeProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockNavigateToUrl).toHaveBeenCalledWith(`${mockCasesUrl}`));
|
||||
await waitFor(() =>
|
||||
expect(mockNavigateToUrl).toHaveBeenCalledWith(`${mockObservabilityUrl}/cases`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect to new case when posting the case', async () => {
|
||||
|
@ -73,7 +75,7 @@ describe('Create case', () => {
|
|||
onSuccess(basicCase);
|
||||
},
|
||||
},
|
||||
application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockCasesUrl },
|
||||
application: { navigateToUrl: mockNavigateToUrl, getUrlForApp: () => mockObservabilityUrl },
|
||||
},
|
||||
});
|
||||
mount(
|
||||
|
@ -85,7 +87,7 @@ describe('Create case', () => {
|
|||
await waitFor(() =>
|
||||
expect(mockNavigateToUrl).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`${mockCasesUrl}${getCaseDetailsUrl({ id: basicCase.id })}`
|
||||
`${mockObservabilityUrl}/cases${getCaseDetailsUrl({ id: basicCase.id })}`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
|
|
@ -10,14 +10,15 @@ import { EuiPanel } from '@elastic/eui';
|
|||
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { getCaseDetailsUrl } from '../../../../pages/cases/links';
|
||||
import { CASES_APP_ID, CASES_OWNER } from '../constants';
|
||||
import { CASES_OWNER } from '../constants';
|
||||
import { observabilityAppId } from '../../../../../common';
|
||||
|
||||
export const Create = React.memo(() => {
|
||||
const {
|
||||
cases,
|
||||
application: { getUrlForApp, navigateToUrl },
|
||||
} = useKibana().services;
|
||||
const casesUrl = getUrlForApp(CASES_APP_ID);
|
||||
const casesUrl = `${getUrlForApp(observabilityAppId)}/cases`;
|
||||
const onSuccess = useCallback(
|
||||
async ({ id }) => navigateToUrl(`${casesUrl}${getCaseDetailsUrl({ id })}`),
|
||||
[casesUrl, navigateToUrl]
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
import { CASES_APP_ID } from '../../common/const';
|
||||
import { casesFeatureId } from '../../common';
|
||||
|
||||
export interface UseGetUserCasesPermissions {
|
||||
crud: boolean;
|
||||
|
@ -20,12 +20,12 @@ export function useGetUserCasesPermissions() {
|
|||
|
||||
useEffect(() => {
|
||||
const capabilitiesCanUserCRUD: boolean =
|
||||
typeof uiCapabilities[CASES_APP_ID].crud_cases === 'boolean'
|
||||
? (uiCapabilities[CASES_APP_ID].crud_cases as boolean)
|
||||
typeof uiCapabilities[casesFeatureId].crud_cases === 'boolean'
|
||||
? (uiCapabilities[casesFeatureId].crud_cases as boolean)
|
||||
: false;
|
||||
const capabilitiesCanUserRead: boolean =
|
||||
typeof uiCapabilities[CASES_APP_ID].read_cases === 'boolean'
|
||||
? (uiCapabilities[CASES_APP_ID].read_cases as boolean)
|
||||
typeof uiCapabilities[casesFeatureId].read_cases === 'boolean'
|
||||
? (uiCapabilities[casesFeatureId].read_cases as boolean)
|
||||
: false;
|
||||
setCasesPermissions({
|
||||
crud: capabilitiesCanUserCRUD,
|
||||
|
|
|
@ -11,14 +11,16 @@ import { useParams } from 'react-router-dom';
|
|||
import { CaseView } from '../../components/app/cases/case_view';
|
||||
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { CASES_APP_ID } from '../../components/app/cases/constants';
|
||||
import { useReadonlyHeader } from '../../hooks/use_readonly_header';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { observabilityAppId } from '../../../common';
|
||||
|
||||
export const CaseDetailsPage = React.memo(() => {
|
||||
const {
|
||||
application: { getUrlForApp, navigateToUrl },
|
||||
} = useKibana().services;
|
||||
const casesUrl = getUrlForApp(CASES_APP_ID);
|
||||
const { ObservabilityPageTemplate } = usePluginContext();
|
||||
const casesUrl = `${getUrlForApp(observabilityAppId)}/cases`;
|
||||
const userPermissions = useGetUserCasesPermissions();
|
||||
const { detailName: caseId, subCaseId } = useParams<{
|
||||
detailName?: string;
|
||||
|
@ -33,7 +35,13 @@ export const CaseDetailsPage = React.memo(() => {
|
|||
}, [casesUrl, navigateToUrl, userPermissions]);
|
||||
|
||||
return caseId != null ? (
|
||||
<CaseView caseId={caseId} subCaseId={subCaseId} userCanCrud={userPermissions?.crud ?? false} />
|
||||
<ObservabilityPageTemplate>
|
||||
<CaseView
|
||||
caseId={caseId}
|
||||
subCaseId={subCaseId}
|
||||
userCanCrud={userPermissions?.crud ?? false}
|
||||
/>
|
||||
</ObservabilityPageTemplate>
|
||||
) : null;
|
||||
});
|
||||
|
||||
|
|
|
@ -10,12 +10,13 @@ import styled from 'styled-components';
|
|||
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import * as i18n from '../../components/app/cases/translations';
|
||||
import { CASES_APP_ID, CASES_OWNER } from '../../components/app/cases/constants';
|
||||
import { CASES_OWNER } from '../../components/app/cases/constants';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
||||
import { casesBreadcrumbs, getCaseUrl, useFormatUrl } from './links';
|
||||
import { observabilityAppId } from '../../../common';
|
||||
|
||||
const ButtonEmpty = styled(EuiButtonEmpty)`
|
||||
display: block;
|
||||
|
@ -25,7 +26,7 @@ function ConfigureCasesPageComponent() {
|
|||
cases,
|
||||
application: { getUrlForApp, navigateToUrl },
|
||||
} = useKibana().services;
|
||||
const casesUrl = getUrlForApp(CASES_APP_ID);
|
||||
const casesUrl = `${getUrlForApp(observabilityAppId)}/cases`;
|
||||
const userPermissions = useGetUserCasesPermissions();
|
||||
const { ObservabilityPageTemplate } = usePluginContext();
|
||||
const onClickGoToCases = useCallback(
|
||||
|
@ -35,7 +36,7 @@ function ConfigureCasesPageComponent() {
|
|||
},
|
||||
[casesUrl, navigateToUrl]
|
||||
);
|
||||
const { formatUrl } = useFormatUrl(CASES_APP_ID);
|
||||
const { formatUrl } = useFormatUrl();
|
||||
const href = formatUrl(getCaseUrl());
|
||||
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.configure]);
|
||||
|
||||
|
|
|
@ -10,12 +10,12 @@ import { EuiButtonEmpty } from '@elastic/eui';
|
|||
import styled from 'styled-components';
|
||||
import * as i18n from '../../components/app/cases/translations';
|
||||
import { Create } from '../../components/app/cases/create';
|
||||
import { CASES_APP_ID } from '../../components/app/cases/constants';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { casesBreadcrumbs, getCaseUrl, useFormatUrl } from './links';
|
||||
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
||||
import { observabilityAppId } from '../../../common';
|
||||
|
||||
const ButtonEmpty = styled(EuiButtonEmpty)`
|
||||
display: block;
|
||||
|
@ -28,7 +28,7 @@ export const CreateCasePage = React.memo(() => {
|
|||
application: { getUrlForApp, navigateToUrl },
|
||||
} = useKibana().services;
|
||||
|
||||
const casesUrl = getUrlForApp(CASES_APP_ID);
|
||||
const casesUrl = `${getUrlForApp(observabilityAppId)}/cases`;
|
||||
const goTo = useCallback(
|
||||
async (ev) => {
|
||||
ev.preventDefault();
|
||||
|
@ -37,7 +37,7 @@ export const CreateCasePage = React.memo(() => {
|
|||
[casesUrl, navigateToUrl]
|
||||
);
|
||||
|
||||
const { formatUrl } = useFormatUrl(CASES_APP_ID);
|
||||
const { formatUrl } = useFormatUrl();
|
||||
const href = formatUrl(getCaseUrl());
|
||||
useBreadcrumbs([{ ...casesBreadcrumbs.cases, href }, casesBreadcrumbs.create]);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { useCallback } from 'react';
|
||||
import { observabilityAppId } from '../../../common';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
|
||||
export const casesBreadcrumbs = {
|
||||
|
@ -39,18 +40,18 @@ interface FormatUrlOptions {
|
|||
}
|
||||
|
||||
export type FormatUrl = (path: string, options?: Partial<FormatUrlOptions>) => string;
|
||||
export const useFormatUrl = (appId: string) => {
|
||||
export const useFormatUrl = () => {
|
||||
const { getUrlForApp } = useKibana().services.application;
|
||||
const formatUrl = useCallback<FormatUrl>(
|
||||
(path: string, { absolute = false } = {}) => {
|
||||
const pathArr = path.split('?');
|
||||
const formattedPath = `${pathArr[0]}${isEmpty(pathArr[1]) ? '' : `?${pathArr[1]}`}`;
|
||||
return getUrlForApp(`${appId}`, {
|
||||
const formattedPath = `/cases/${pathArr[0]}${isEmpty(pathArr[1]) ? '' : `?${pathArr[1]}`}`;
|
||||
return getUrlForApp(observabilityAppId, {
|
||||
path: formattedPath,
|
||||
absolute,
|
||||
});
|
||||
},
|
||||
[appId, getUrlForApp]
|
||||
[getUrlForApp]
|
||||
);
|
||||
return { formatUrl };
|
||||
};
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import {
|
||||
TriggersAndActionsUIPublicPluginSetup,
|
||||
TriggersAndActionsUIPublicPluginStart,
|
||||
} from '../../triggers_actions_ui/public';
|
||||
import { BehaviorSubject, from } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ConfigSchema } from '.';
|
||||
import {
|
||||
AppDeepLink,
|
||||
AppMountParameters,
|
||||
AppNavLinkStatus,
|
||||
AppUpdater,
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
|
@ -28,16 +28,19 @@ import type {
|
|||
HomePublicPluginSetup,
|
||||
HomePublicPluginStart,
|
||||
} from '../../../../src/plugins/home/public';
|
||||
import type { LensPublicStart } from '../../lens/public';
|
||||
import { registerDataHandler } from './data_handler';
|
||||
import { createCallObservabilityApi } from './services/call_observability_api';
|
||||
import { createNavigationRegistry } from './services/navigation_registry';
|
||||
import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav';
|
||||
import { ConfigSchema } from '.';
|
||||
import { createObservabilityRuleTypeRegistry } from './rules/create_observability_rule_type_registry';
|
||||
import { createLazyObservabilityPageTemplate } from './components/shared';
|
||||
import { CASES_APP_ID } from './components/app/cases/constants';
|
||||
import { CasesUiStart } from '../../cases/public';
|
||||
import type { LensPublicStart } from '../../lens/public';
|
||||
import {
|
||||
TriggersAndActionsUIPublicPluginSetup,
|
||||
TriggersAndActionsUIPublicPluginStart,
|
||||
} from '../../triggers_actions_ui/public';
|
||||
import { observabilityAppId, observabilityFeatureId } from '../common';
|
||||
import { createLazyObservabilityPageTemplate } from './components/shared';
|
||||
import { registerDataHandler } from './data_handler';
|
||||
import { createObservabilityRuleTypeRegistry } from './rules/create_observability_rule_type_registry';
|
||||
import { createCallObservabilityApi } from './services/call_observability_api';
|
||||
import { createNavigationRegistry, NavigationEntry } from './services/navigation_registry';
|
||||
import { updateGlobalNavigation } from './update_global_navigation';
|
||||
|
||||
export type ObservabilityPublicSetup = ReturnType<Plugin['setup']>;
|
||||
|
||||
|
@ -66,9 +69,31 @@ export class Plugin
|
|||
ObservabilityPublicPluginsStart
|
||||
> {
|
||||
private readonly appUpdater$ = new BehaviorSubject<AppUpdater>(() => ({}));
|
||||
private readonly casesAppUpdater$ = new BehaviorSubject<AppUpdater>(() => ({}));
|
||||
private readonly navigationRegistry = createNavigationRegistry();
|
||||
|
||||
// Define deep links as constant and hidden. Whether they are shown or hidden
|
||||
// in the global navigation will happen in `updateGlobalNavigation`.
|
||||
private readonly deepLinks: AppDeepLink[] = [
|
||||
{
|
||||
id: 'alerts',
|
||||
title: i18n.translate('xpack.observability.alertsLinkTitle', {
|
||||
defaultMessage: 'Alerts',
|
||||
}),
|
||||
order: 8001,
|
||||
path: '/alerts',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
{
|
||||
id: 'cases',
|
||||
title: i18n.translate('xpack.observability.casesLinkTitle', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
order: 8002,
|
||||
path: '/cases',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
];
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext<ConfigSchema>) {
|
||||
this.initializerContext = initializerContext;
|
||||
}
|
||||
|
@ -103,47 +128,26 @@ export class Plugin
|
|||
});
|
||||
};
|
||||
|
||||
const updater$ = this.appUpdater$;
|
||||
|
||||
coreSetup.application.register({
|
||||
id: 'observability-overview',
|
||||
title: 'Overview',
|
||||
const appUpdater$ = this.appUpdater$;
|
||||
const app = {
|
||||
appRoute: '/app/observability',
|
||||
order: 8000,
|
||||
category,
|
||||
deepLinks: this.deepLinks,
|
||||
euiIconType,
|
||||
id: observabilityAppId,
|
||||
mount,
|
||||
updater$,
|
||||
});
|
||||
if (config.unsafe.alertingExperience.enabled) {
|
||||
coreSetup.application.register({
|
||||
id: 'observability-alerts',
|
||||
title: 'Alerts',
|
||||
appRoute: '/app/observability/alerts',
|
||||
order: 8025,
|
||||
category,
|
||||
euiIconType,
|
||||
mount,
|
||||
updater$,
|
||||
});
|
||||
}
|
||||
order: 8000,
|
||||
title: i18n.translate('xpack.observability.overviewLinkTitle', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
updater$: appUpdater$,
|
||||
};
|
||||
|
||||
if (config.unsafe.cases.enabled) {
|
||||
coreSetup.application.register({
|
||||
id: CASES_APP_ID,
|
||||
title: 'Cases',
|
||||
appRoute: '/app/observability/cases',
|
||||
order: 8050,
|
||||
category,
|
||||
euiIconType,
|
||||
mount,
|
||||
updater$: this.casesAppUpdater$,
|
||||
});
|
||||
}
|
||||
coreSetup.application.register(app);
|
||||
|
||||
if (pluginsSetup.home) {
|
||||
pluginsSetup.home.featureCatalogue.registerSolution({
|
||||
id: 'observability',
|
||||
id: observabilityFeatureId,
|
||||
title: i18n.translate('xpack.observability.featureCatalogueTitle', {
|
||||
defaultMessage: 'Observability',
|
||||
}),
|
||||
|
@ -172,13 +176,47 @@ export class Plugin
|
|||
}
|
||||
|
||||
this.navigationRegistry.registerSections(
|
||||
of([
|
||||
{
|
||||
label: '',
|
||||
sortKey: 100,
|
||||
entries: [{ label: 'Overview', app: 'observability-overview', path: '/overview' }],
|
||||
},
|
||||
])
|
||||
from(appUpdater$).pipe(
|
||||
map((value) => {
|
||||
const deepLinks = value(app)?.deepLinks ?? [];
|
||||
|
||||
const overviewLink = {
|
||||
label: i18n.translate('xpack.observability.overviewLinkTitle', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
app: observabilityAppId,
|
||||
path: '/overview',
|
||||
};
|
||||
|
||||
// Reformat the visible links to be NavigationEntry objects instead of
|
||||
// AppDeepLink objects.
|
||||
//
|
||||
// In our case the deep links and sections being registered are the
|
||||
// same, and the logic to hide them based on flags or capabilities is
|
||||
// the same, so we just want to make a new list with the properties
|
||||
// needed by `registerSections`, which are different than the
|
||||
// properties used by the deepLinks.
|
||||
//
|
||||
// See https://github.com/elastic/kibana/issues/103325.
|
||||
const otherLinks: NavigationEntry[] = deepLinks
|
||||
.filter((link) => link.navLinkStatus === AppNavLinkStatus.visible)
|
||||
.map((link) => ({
|
||||
app: observabilityAppId,
|
||||
label: link.title,
|
||||
path: link.path ?? '',
|
||||
}));
|
||||
|
||||
const sections = [
|
||||
{
|
||||
label: '',
|
||||
sortKey: 100,
|
||||
entries: [overviewLink, ...otherLinks],
|
||||
},
|
||||
];
|
||||
|
||||
return sections;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -190,8 +228,16 @@ export class Plugin
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
public start({ application }: CoreStart) {
|
||||
toggleOverviewLinkInNav(this.appUpdater$, this.casesAppUpdater$, application);
|
||||
const config = this.initializerContext.config.get();
|
||||
|
||||
updateGlobalNavigation({
|
||||
capabilities: application.capabilities,
|
||||
config,
|
||||
deepLinks: this.deepLinks,
|
||||
updater$: this.appUpdater$,
|
||||
});
|
||||
|
||||
const PageTemplate = createLazyObservabilityPageTemplate({
|
||||
currentAppId$: application.currentAppId$,
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { AppUpdater, AppNavLinkStatus } from '../../../../src/core/public';
|
||||
import { applicationServiceMock } from '../../../../src/core/public/mocks';
|
||||
|
||||
import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav';
|
||||
|
||||
describe('toggleOverviewLinkInNav', () => {
|
||||
let applicationStart: ReturnType<typeof applicationServiceMock.createStartContract>;
|
||||
let subjectMock: jest.Mocked<Subject<AppUpdater>>;
|
||||
let casesMock: jest.Mocked<Subject<AppUpdater>>;
|
||||
|
||||
beforeEach(() => {
|
||||
applicationStart = applicationServiceMock.createStartContract();
|
||||
subjectMock = {
|
||||
next: jest.fn(),
|
||||
} as any;
|
||||
});
|
||||
|
||||
it('hides overview menu', () => {
|
||||
applicationStart.capabilities = {
|
||||
management: {},
|
||||
catalogue: {},
|
||||
navLinks: {
|
||||
apm: false,
|
||||
logs: false,
|
||||
metrics: false,
|
||||
uptime: false,
|
||||
},
|
||||
};
|
||||
|
||||
toggleOverviewLinkInNav(subjectMock, casesMock, applicationStart);
|
||||
|
||||
expect(subjectMock.next).toHaveBeenCalledTimes(1);
|
||||
const updater = subjectMock.next.mock.calls[0][0]!;
|
||||
expect(updater({} as any)).toEqual({
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
});
|
||||
});
|
||||
it('shows overview menu', () => {
|
||||
applicationStart.capabilities = {
|
||||
management: {},
|
||||
catalogue: {},
|
||||
navLinks: {
|
||||
apm: true,
|
||||
logs: false,
|
||||
metrics: false,
|
||||
uptime: false,
|
||||
},
|
||||
};
|
||||
|
||||
toggleOverviewLinkInNav(subjectMock, casesMock, applicationStart);
|
||||
|
||||
expect(subjectMock.next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { AppNavLinkStatus, AppUpdater, ApplicationStart } from '../../../../src/core/public';
|
||||
import { CASES_APP_ID } from '../common/const';
|
||||
|
||||
export function toggleOverviewLinkInNav(
|
||||
updater$: Subject<AppUpdater>,
|
||||
casesUpdater$: Subject<AppUpdater>,
|
||||
{ capabilities }: ApplicationStart
|
||||
) {
|
||||
const { apm, logs, metrics, uptime, [CASES_APP_ID]: cases } = capabilities.navLinks;
|
||||
const someVisible = Object.values({ apm, logs, metrics, uptime }).some((visible) => visible);
|
||||
|
||||
// if cases is enabled then we want to show it in the sidebar but not the navigation unless one of the other features
|
||||
// is enabled
|
||||
if (cases) {
|
||||
casesUpdater$.next(() => ({ navLinkStatus: AppNavLinkStatus.visible }));
|
||||
}
|
||||
|
||||
if (!someVisible) {
|
||||
updater$.next(() => ({
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,269 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { ConfigSchema } from '.';
|
||||
import {
|
||||
App,
|
||||
AppDeepLink,
|
||||
ApplicationStart,
|
||||
AppNavLinkStatus,
|
||||
AppUpdater,
|
||||
} from '../../../../src/core/public';
|
||||
import { casesFeatureId } from '../common';
|
||||
import { updateGlobalNavigation } from './update_global_navigation';
|
||||
|
||||
// Used in updater callback
|
||||
const app = ({} as unknown) as App;
|
||||
|
||||
describe('updateGlobalNavigation', () => {
|
||||
describe('when no observability apps are enabled', () => {
|
||||
it('hides the overview link', () => {
|
||||
const capabilities = ({
|
||||
navLinks: { apm: false, logs: false, metrics: false, uptime: false },
|
||||
} as unknown) as ApplicationStart['capabilities'];
|
||||
const config = {
|
||||
unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } },
|
||||
} as ConfigSchema;
|
||||
const deepLinks: AppDeepLink[] = [];
|
||||
const callback = jest.fn();
|
||||
const updater$ = ({
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown) as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, config, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when one observability app is enabled', () => {
|
||||
it('shows the overview link', () => {
|
||||
const capabilities = ({
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown) as ApplicationStart['capabilities'];
|
||||
const config = {
|
||||
unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } },
|
||||
} as ConfigSchema;
|
||||
const deepLinks: AppDeepLink[] = [];
|
||||
const callback = jest.fn();
|
||||
const updater$ = ({
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown) as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, config, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when cases are enabled', () => {
|
||||
it('shows the cases deep link', () => {
|
||||
const capabilities = ({
|
||||
[casesFeatureId]: { read_cases: true },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown) as ApplicationStart['capabilities'];
|
||||
const config = {
|
||||
unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } },
|
||||
} as ConfigSchema;
|
||||
const deepLinks = [
|
||||
{
|
||||
id: 'cases',
|
||||
title: 'Cases',
|
||||
order: 8002,
|
||||
path: '/cases',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
];
|
||||
const callback = jest.fn();
|
||||
const updater$ = ({
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown) as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, config, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'cases',
|
||||
title: 'Cases',
|
||||
order: 8002,
|
||||
path: '/cases',
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
},
|
||||
],
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when cases are disabled', () => {
|
||||
it('hides the cases deep link', () => {
|
||||
const capabilities = ({
|
||||
[casesFeatureId]: { read_cases: true },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown) as ApplicationStart['capabilities'];
|
||||
const config = {
|
||||
unsafe: { alertingExperience: { enabled: true }, cases: { enabled: false } },
|
||||
} as ConfigSchema;
|
||||
const deepLinks = [
|
||||
{
|
||||
id: 'cases',
|
||||
title: 'Cases',
|
||||
order: 8002,
|
||||
path: '/cases',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
];
|
||||
const callback = jest.fn();
|
||||
const updater$ = ({
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown) as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, config, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'cases',
|
||||
title: 'Cases',
|
||||
order: 8002,
|
||||
path: '/cases',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
],
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no case read capabilities', () => {
|
||||
it('hides the cases deep link', () => {
|
||||
const capabilities = ({
|
||||
[casesFeatureId]: { read_cases: false },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown) as ApplicationStart['capabilities'];
|
||||
const config = {
|
||||
unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } },
|
||||
} as ConfigSchema;
|
||||
const deepLinks = [
|
||||
{
|
||||
id: 'cases',
|
||||
title: 'Cases',
|
||||
order: 8002,
|
||||
path: '/cases',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
];
|
||||
const callback = jest.fn();
|
||||
const updater$ = ({
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown) as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, config, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'cases',
|
||||
title: 'Cases',
|
||||
order: 8002,
|
||||
path: '/cases',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
],
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when alerts are enabled', () => {
|
||||
it('shows the alerts deep link', () => {
|
||||
const capabilities = ({
|
||||
[casesFeatureId]: { read_cases: true },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown) as ApplicationStart['capabilities'];
|
||||
const config = {
|
||||
unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } },
|
||||
} as ConfigSchema;
|
||||
const deepLinks = [
|
||||
{
|
||||
id: 'alerts',
|
||||
title: 'Alerts',
|
||||
order: 8001,
|
||||
path: '/alerts',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
];
|
||||
const callback = jest.fn();
|
||||
const updater$ = ({
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown) as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, config, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'alerts',
|
||||
title: 'Alerts',
|
||||
order: 8001,
|
||||
path: '/alerts',
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
},
|
||||
],
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when alerts are disabled', () => {
|
||||
it('hides the alerts deep link', () => {
|
||||
const capabilities = ({
|
||||
[casesFeatureId]: { read_cases: true },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown) as ApplicationStart['capabilities'];
|
||||
const config = {
|
||||
unsafe: { alertingExperience: { enabled: false }, cases: { enabled: false } },
|
||||
} as ConfigSchema;
|
||||
const deepLinks = [
|
||||
{
|
||||
id: 'alerts',
|
||||
title: 'Alerts',
|
||||
order: 8001,
|
||||
path: '/alerts',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
];
|
||||
const callback = jest.fn();
|
||||
const updater$ = ({
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown) as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, config, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'alerts',
|
||||
title: 'Alerts',
|
||||
order: 8001,
|
||||
path: '/alerts',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
},
|
||||
],
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { ConfigSchema } from '.';
|
||||
import {
|
||||
AppNavLinkStatus,
|
||||
AppUpdater,
|
||||
ApplicationStart,
|
||||
AppDeepLink,
|
||||
} from '../../../../src/core/public';
|
||||
import { casesFeatureId } from '../common';
|
||||
|
||||
export function updateGlobalNavigation({
|
||||
capabilities,
|
||||
config,
|
||||
deepLinks,
|
||||
updater$,
|
||||
}: {
|
||||
capabilities: ApplicationStart['capabilities'];
|
||||
config: ConfigSchema;
|
||||
deepLinks: AppDeepLink[];
|
||||
updater$: Subject<AppUpdater>;
|
||||
}) {
|
||||
const { apm, logs, metrics, uptime } = capabilities.navLinks;
|
||||
const someVisible = Object.values({ apm, logs, metrics, uptime }).some((visible) => visible);
|
||||
|
||||
const updatedDeepLinks = deepLinks.map((link) => {
|
||||
switch (link.id) {
|
||||
case 'cases':
|
||||
return {
|
||||
...link,
|
||||
navLinkStatus:
|
||||
config.unsafe.cases.enabled && capabilities[casesFeatureId].read_cases && someVisible
|
||||
? AppNavLinkStatus.visible
|
||||
: AppNavLinkStatus.hidden,
|
||||
};
|
||||
case 'alerts':
|
||||
return {
|
||||
...link,
|
||||
navLinkStatus:
|
||||
config.unsafe.alertingExperience.enabled && someVisible
|
||||
? AppNavLinkStatus.visible
|
||||
: AppNavLinkStatus.hidden,
|
||||
};
|
||||
default:
|
||||
return link;
|
||||
}
|
||||
});
|
||||
|
||||
updater$.next(() => ({
|
||||
deepLinks: updatedDeepLinks,
|
||||
navLinkStatus: someVisible ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden,
|
||||
}));
|
||||
}
|
|
@ -24,7 +24,7 @@ import { PluginSetupContract as FeaturesSetup } from '../../features/server';
|
|||
import { uiSettings } from './ui_settings';
|
||||
import { registerRoutes } from './routes/register_routes';
|
||||
import { getGlobalObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository';
|
||||
import { CASES_APP_ID, OBSERVABILITY } from '../common/const';
|
||||
import { casesFeatureId, observabilityFeatureId } from '../common';
|
||||
|
||||
export type ObservabilityPluginSetup = ReturnType<ObservabilityPlugin['setup']>;
|
||||
|
||||
|
@ -40,41 +40,41 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
|
||||
public setup(core: CoreSetup, plugins: PluginSetup) {
|
||||
plugins.features.registerKibanaFeature({
|
||||
id: CASES_APP_ID,
|
||||
id: casesFeatureId,
|
||||
name: i18n.translate('xpack.observability.featureRegistry.linkObservabilityTitle', {
|
||||
defaultMessage: 'Cases',
|
||||
}),
|
||||
order: 1100,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
app: [CASES_APP_ID, 'kibana'],
|
||||
catalogue: [OBSERVABILITY],
|
||||
cases: [OBSERVABILITY],
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: [observabilityFeatureId],
|
||||
privileges: {
|
||||
all: {
|
||||
app: [CASES_APP_ID, 'kibana'],
|
||||
catalogue: [OBSERVABILITY],
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: {
|
||||
all: [OBSERVABILITY],
|
||||
all: [observabilityFeatureId],
|
||||
},
|
||||
api: [],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['crud_cases', 'read_cases'], // uiCapabilities[CASES_APP_ID].crud_cases or read_cases
|
||||
ui: ['crud_cases', 'read_cases'], // uiCapabilities[casesFeatureId].crud_cases or read_cases
|
||||
},
|
||||
read: {
|
||||
app: [CASES_APP_ID, 'kibana'],
|
||||
catalogue: [OBSERVABILITY],
|
||||
app: [casesFeatureId, 'kibana'],
|
||||
catalogue: [observabilityFeatureId],
|
||||
cases: {
|
||||
read: [OBSERVABILITY],
|
||||
read: [observabilityFeatureId],
|
||||
},
|
||||
api: [],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: ['read_cases'], // uiCapabilities[uiCapabilities[CASES_APP_ID]].read_cases
|
||||
ui: ['read_cases'], // uiCapabilities[uiCapabilities[casesFeatureId]].read_cases
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import { isoToEpochRt, toNumberRt } from '@kbn/io-ts-utils';
|
||||
import * as t from 'io-ts';
|
||||
import { observabilityFeatureId } from '../../common';
|
||||
import { alertStatusRt } from '../../common/typings';
|
||||
import { getTopAlerts } from '../lib/rules/get_top_alerts';
|
||||
import { createObservabilityServerRoute } from './create_observability_server_route';
|
||||
|
@ -51,7 +52,7 @@ const alertsDynamicIndexPatternRoute = createObservabilityServerRoute({
|
|||
tags: [],
|
||||
},
|
||||
handler: async ({ ruleDataClient }) => {
|
||||
const reader = ruleDataClient.getReader({ namespace: 'observability' });
|
||||
const reader = ruleDataClient.getReader({ namespace: observabilityFeatureId });
|
||||
|
||||
return reader.getDynamicIndexPattern();
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiSettingsParams } from '../../../../src/core/types';
|
||||
import { observabilityFeatureId } from '../common';
|
||||
import { enableInspectEsQueries } from '../common/ui_settings_keys';
|
||||
|
||||
/**
|
||||
|
@ -15,7 +16,7 @@ import { enableInspectEsQueries } from '../common/ui_settings_keys';
|
|||
*/
|
||||
export const uiSettings: Record<string, UiSettingsParams<boolean>> = {
|
||||
[enableInspectEsQueries]: {
|
||||
category: ['observability'],
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.observability.enableInspectEsQueriesExperimentName', {
|
||||
defaultMessage: 'inspect ES queries',
|
||||
}),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue