From 86e7aca951f1d66dd6b1ccadd7771985dd8f63de Mon Sep 17 00:00:00 2001 From: Yngrid Coello Date: Fri, 14 Jul 2023 11:21:22 +0200 Subject: [PATCH] [Logs onboarding] Add client side routing support for individual steps (#161230) Closes https://github.com/elastic/kibana/issues/159500. This PR focuses on adding client-site routing support to the wizard developed for observability_onboarding plugin. ### Changes - Added routes for each step of the wizard. - Added support for `history.push` and `history.goBack` in wizard context. - Added basePath to wizard provider. https://github.com/elastic/kibana/assets/1313018/c32e25b3-2470-41a4-8c34-f2dd5ea7c366 --- .../public/application/app.tsx | 29 +++- .../app/custom_logs/wizard/back_button.tsx | 29 ++++ .../app/custom_logs/wizard/configure_logs.tsx | 11 +- .../app/custom_logs/wizard/index.tsx | 36 +++-- .../app/custom_logs/wizard/inspect.tsx | 13 +- .../wizard/install_elastic_agent.tsx | 45 ++---- .../public/context/create_wizard_context.tsx | 136 +++++++++++------- .../public/context/nav_events.ts | 58 ++++++++ .../public/context/path.ts | 22 +++ .../public/routes/index.tsx | 24 ++-- .../templates/custom_logs.tsx} | 26 ++-- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 14 files changed, 283 insertions(+), 149 deletions(-) create mode 100644 x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/back_button.tsx create mode 100644 x-pack/plugins/observability_onboarding/public/context/nav_events.ts create mode 100644 x-pack/plugins/observability_onboarding/public/context/path.ts rename x-pack/plugins/observability_onboarding/public/{components/app/custom_logs/index.tsx => routes/templates/custom_logs.tsx} (79%) diff --git a/x-pack/plugins/observability_onboarding/public/application/app.tsx b/x-pack/plugins/observability_onboarding/public/application/app.tsx index af96fa1a25a4b..d04408545b30a 100644 --- a/x-pack/plugins/observability_onboarding/public/application/app.tsx +++ b/x-pack/plugins/observability_onboarding/public/application/app.tsx @@ -25,12 +25,14 @@ import { euiDarkVars, euiLightVars } from '@kbn/ui-theme'; import React from 'react'; import ReactDOM from 'react-dom'; import { RouteComponentProps, RouteProps } from 'react-router-dom'; +import { customLogsRoutes } from '../components/app/custom_logs/wizard'; import { ObservabilityOnboardingHeaderActionMenu } from '../components/app/header_action_menu'; import { ObservabilityOnboardingPluginSetupDeps, ObservabilityOnboardingPluginStartDeps, } from '../plugin'; -import { routes } from '../routes'; +import { baseRoutes, routes } from '../routes'; +import { CustomLogs } from '../routes/templates/custom_logs'; export type BreadcrumbTitle< T extends { [K in keyof T]?: string | undefined } = {} @@ -55,19 +57,42 @@ export const breadcrumbsApp = { }; function App() { + const customLogRoutesPaths = Object.keys(customLogsRoutes); + return ( <> - {Object.keys(routes).map((key) => { + {Object.keys(baseRoutes).map((key) => { const path = key as keyof typeof routes; const { handler, exact } = routes[path]; const Wrapper = () => { return handler(); }; + return ( ); })} + + + {customLogRoutesPaths.map((key) => { + const path = key as keyof typeof routes; + const { handler, exact } = routes[path]; + const Wrapper = () => { + return handler(); + }; + + return ( + + ); + })} + + ); diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/back_button.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/back_button.tsx new file mode 100644 index 0000000000000..e4c7172d8475f --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/back_button.tsx @@ -0,0 +1,29 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { useWizard } from '.'; + +export function BackButton({ onBack }: { onBack: () => void }) { + const { getPath } = useWizard(); + const history = useHistory(); + + return ( + + {i18n.translate('xpack.observability_onboarding.steps.back', { + defaultMessage: 'Back', + })} + + ); +} diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx index ea78c4cf4acb4..4d8158eb4a8c8 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx @@ -33,6 +33,7 @@ import { StepPanelContent, StepPanelFooter, } from '../../../shared/step_panel'; +import { BackButton } from './back_button'; import { getFilename, replaceSpecialChars } from './get_filename'; export function ConfigureLogs() { @@ -51,10 +52,6 @@ export function ConfigureLogs() { const logFilePathNotConfigured = logFilePaths.every((filepath) => !filepath); - function onBack() { - goBack(); - } - function onContinue() { setState((state) => ({ ...state, @@ -100,11 +97,7 @@ export function ConfigureLogs() { panelFooter={ - {i18n.translate('xpack.observability_onboarding.steps.back', { - defaultMessage: 'Back', - })} - , + , > = { + selectLogs: SelectLogs, + configureLogs: ConfigureLogs, + installElasticAgent: InstallElasticAgent, + inspect: Inspect, +}; + +const { + Provider, + useWizard, + routes: customLogsRoutes, +} = createWizardContext({ initialState, initialStep: 'selectLogs', - steps: { - selectLogs: SelectLogs, - configureLogs: ConfigureLogs, - installElasticAgent: InstallElasticAgent, - inspect: Inspect, - }, + steps, + basePath: '/customLogs', }); -export { Provider, Step, useWizard }; +export { Provider, useWizard, customLogsRoutes }; diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/inspect.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/inspect.tsx index 72f05c65c7564..2a9074e6976e1 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/inspect.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/inspect.tsx @@ -6,28 +6,21 @@ */ import React from 'react'; -import { EuiButton, EuiTitle, EuiSpacer } from '@elastic/eui'; +import { EuiTitle, EuiSpacer } from '@elastic/eui'; import { StepPanel, StepPanelContent, StepPanelFooter, } from '../../../shared/step_panel'; import { useWizard } from '.'; +import { BackButton } from './back_button'; export function Inspect() { const { goBack, getState, getPath, getUsage } = useWizard(); return ( - Back - , - ]} - /> - } + panelFooter={]} />} > diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/install_elastic_agent.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/install_elastic_agent.tsx index 8bfff0f57c4f4..5c4ba0cbf3049 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/install_elastic_agent.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/install_elastic_agent.tsx @@ -35,11 +35,12 @@ import { StepPanelFooter, } from '../../../shared/step_panel'; import { ApiKeyBanner } from './api_key_banner'; +import { BackButton } from './back_button'; type ElasticAgentPlatform = 'linux-tar' | 'macos' | 'windows'; export function InstallElasticAgent() { const { navigateToKibanaUrl } = useKibanaNavigation(); - const { goBack, goToStep, getState, setState, CurrentStep } = useWizard(); + const { goBack, goToStep, getState, setState } = useWizard(); const wizardState = getState(); const [elasticAgentPlatform, setElasticAgentPlatform] = useState('linux-tar'); @@ -51,10 +52,6 @@ export function InstallElasticAgent() { navigateToKibanaUrl('/app/logs/stream'); } - function onBack() { - goBack(); - } - function onAutoDownloadConfig() { setState((state) => ({ ...state, @@ -64,10 +61,7 @@ export function InstallElasticAgent() { const { data: monitoringRole, status: monitoringRoleStatus } = useFetcher( (callApi) => { - if ( - CurrentStep === InstallElasticAgent && - !hasAlreadySavedFlow(getState()) - ) { + if (!hasAlreadySavedFlow(getState())) { return callApi( 'GET /internal/observability_onboarding/custom_logs/privileges' ); @@ -77,11 +71,9 @@ export function InstallElasticAgent() { ); const { data: setup } = useFetcher((callApi) => { - if (CurrentStep === InstallElasticAgent) { - return callApi( - 'GET /internal/observability_onboarding/custom_logs/install_shipper_setup' - ); - } + return callApi( + 'GET /internal/observability_onboarding/custom_logs/install_shipper_setup' + ); }, []); const { @@ -97,11 +89,7 @@ export function InstallElasticAgent() { customConfigurations, logFilePaths, } = getState(); - if ( - CurrentStep === InstallElasticAgent && - !hasAlreadySavedFlow(getState()) && - monitoringRole?.hasPrivileges - ) { + if (!hasAlreadySavedFlow(getState()) && monitoringRole?.hasPrivileges) { return callApi( 'POST /internal/observability_onboarding/custom_logs/save', { @@ -133,7 +121,7 @@ export function InstallElasticAgent() { customConfigurations, logFilePaths, } = getState(); - if (CurrentStep === InstallElasticAgent && onboardingId) { + if (onboardingId) { return callApi( 'PUT /internal/observability_onboarding/custom_logs/{onboardingId}/save', { @@ -158,11 +146,7 @@ export function InstallElasticAgent() { const { data: yamlConfig = '', status: yamlConfigStatus } = useFetcher( (callApi) => { - if ( - CurrentStep === InstallElasticAgent && - apiKeyEncoded && - onboardingId - ) { + if (apiKeyEncoded && onboardingId) { return callApi( 'GET /internal/observability_onboarding/elastic_agent/config', { @@ -190,7 +174,7 @@ export function InstallElasticAgent() { refetch: refetchProgress, } = useFetcher( (callApi) => { - if (CurrentStep === InstallElasticAgent && onboardingId) { + if (onboardingId) { return callApi( 'GET /internal/observability_onboarding/custom_logs/{onboardingId}/progress', { params: { path: { onboardingId } } } @@ -208,8 +192,7 @@ export function InstallElasticAgent() { refetchProgress(); }, 2000); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [progressSucceded]); + }, [progressSucceded, refetchProgress]); const getStep = useCallback( ({ id, incompleteTitle, loadingTitle, completedTitle }) => { @@ -270,11 +253,7 @@ export function InstallElasticAgent() { panelFooter={ - {i18n.translate('xpack.observability_onboarding.steps.back', { - defaultMessage: 'Back', - })} - , + , diff --git a/x-pack/plugins/observability_onboarding/public/context/create_wizard_context.tsx b/x-pack/plugins/observability_onboarding/public/context/create_wizard_context.tsx index f1bfebf65d309..9347f45846ffc 100644 --- a/x-pack/plugins/observability_onboarding/public/context/create_wizard_context.tsx +++ b/x-pack/plugins/observability_onboarding/public/context/create_wizard_context.tsx @@ -12,10 +12,16 @@ import React, { useContext, useState, useRef, + useEffect, } from 'react'; +import { useHistory } from 'react-router-dom'; +import { generateNavEvents, NavEvent } from './nav_events'; +import { generatePath } from './path'; + +type Entry = { [K in keyof T]: [K, T[K]] }[keyof T]; export interface WizardContext { - CurrentStep: ComponentType; + setCurrentStep: (step: StepKey) => void; goToStep: (step: StepKey) => void; goBack: () => void; getState: () => T; @@ -40,27 +46,66 @@ export function createWizardContext< initialState, initialStep, steps, + basePath, }: { initialState: T; initialStep: InitialStepKey; steps: Record; + basePath: string; }) { const context = createContext>({ - CurrentStep: () => null, + setCurrentStep: () => {}, goToStep: () => {}, goBack: () => {}, getState: () => initialState, setState: () => {}, getPath: () => [], - getUsage: () => ({ timeSinceStart: 0, navEvents: [] }), + getUsage: () => ({ + timeSinceStart: 0, + navEvents: new Array>(), + }), }); + const stepRoute = (stepKey: StepKey) => + stepKey === initialStep ? basePath : `${basePath}/${stepKey}`; + + const routes = Object.entries(steps).reduce((acc, pair) => { + const [key, value] = pair as Entry>; + return { + ...acc, + [stepRoute(key)]: { + exact: true, + handler: () => + Page({ + step: key, + Component: value, + }), + }, + }; + }, {}); + + function Page({ + step, + Component, + }: { + step: StepKey; + Component: ComponentType; + }) { + const { setCurrentStep } = useWizard(); + useEffect(() => { + setCurrentStep(step); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [step]); + + return ; + } + function Provider({ children, onChangeStep, transitionDuration, }: { - children?: ReactNode; + children: ReactNode; onChangeStep?: (stepChangeEvent: { direction: 'back' | 'next'; stepKey: StepKey; @@ -68,79 +113,69 @@ export function createWizardContext< }) => void; transitionDuration?: number; }) { - const [step, setStep] = useState(initialStep); - const pathRef = useRef([initialStep]); + const history = useHistory(); + const [step, setStep] = useState(); + const pathRef = useRef([]); const usageRef = useRef['getUsage']>>({ timeSinceStart: 0, - navEvents: [ - { type: 'initial', step, timestamp: Date.now(), duration: 0 }, - ], + navEvents: new Array>(), }); const [state, setState] = useState(initialState); + return ( key === stepKey); + + pathRef.current = generatePath({ + step: stepKey, + path: pathRef.current, + }); + + usageRef.current.navEvents = generateNavEvents({ + type: stepVisited ? 'back' : 'progress', step: stepKey, - timestamp, - duration: 0, + navEvents: usageRef.current.navEvents as Array>, }); + if (onChangeStep) { onChangeStep({ - direction: 'next', + direction: stepVisited ? 'back' : 'next', stepKey, StepComponent: steps[stepKey], }); } + }, + goToStep(stepKey: StepKey) { + if (stepKey === step) { + return; + } + const stepUrl = stepRoute(stepKey); + if (transitionDuration) { setTimeout(() => { - setStep(stepKey); + history.push(stepUrl); }, transitionDuration); } else { - setStep(stepKey); + history.push(stepUrl); } }, goBack() { - if (step === initialStep) { + if (history.length === 1 || pathRef.current.length === 1) { return; } - const path = pathRef.current; - path.pop(); - const lastStep = path[path.length - 1]; - const navEvents = usageRef.current.navEvents; - const currentNavEvent = navEvents[navEvents.length - 1]; - const timestamp = Date.now(); - currentNavEvent.duration = timestamp - currentNavEvent.timestamp; - usageRef.current.navEvents.push({ - type: 'back', - step: lastStep, - timestamp, - duration: 0, - }); - if (onChangeStep) { - onChangeStep({ - direction: 'back', - stepKey: lastStep, - StepComponent: steps[lastStep], - }); - } if (transitionDuration) { setTimeout(() => { - setStep(lastStep); + history.goBack(); }, transitionDuration); } else { - setStep(lastStep); + history.goBack(); } }, getState: () => state as T, @@ -166,16 +201,9 @@ export function createWizardContext< ); } - function Step() { - const { CurrentStep } = useContext(context); - return ; - } - function useWizard() { - // const { CurrentStep: _, ...rest } = useContext(context); - // return rest; return useContext(context); } - return { context, Provider, Step, useWizard }; + return { context, Provider, useWizard, routes }; } diff --git a/x-pack/plugins/observability_onboarding/public/context/nav_events.ts b/x-pack/plugins/observability_onboarding/public/context/nav_events.ts new file mode 100644 index 0000000000000..310a03fc7e49c --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/context/nav_events.ts @@ -0,0 +1,58 @@ +/* + * 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. + */ + +type NavEventType = 'inital' | 'back' | 'progress'; + +export interface NavEvent { + type: NavEventType; + step: StepKey; + timestamp: number; + duration: number; +} + +const createEvent = ({ + type, + step, + timestamp, +}: { + type: NavEventType; + step: StepKey; + timestamp?: number; +}) => ({ + type, + step, + timestamp: timestamp ?? Date.now(), + duration: 0, +}); + +export const generateNavEvents = ({ + type, + step, + navEvents, +}: { + type: NavEventType; + step: StepKey; + navEvents: Array>; +}) => { + if (navEvents.length === 0) { + return [createEvent({ type: 'inital', step })]; + } + + const mutableNavEvents = [...navEvents]; + const previousEvent = mutableNavEvents[navEvents.length - 1]; + const timestamp = Date.now(); + previousEvent.duration = timestamp - previousEvent.timestamp; + + return [ + ...mutableNavEvents, + createEvent({ + type, + step, + timestamp, + }), + ]; +}; diff --git a/x-pack/plugins/observability_onboarding/public/context/path.ts b/x-pack/plugins/observability_onboarding/public/context/path.ts new file mode 100644 index 0000000000000..2530ae17c886c --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/context/path.ts @@ -0,0 +1,22 @@ +/* + * 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 generatePath = ({ + step, + path, +}: { + step: StepKey; + path: StepKey[]; +}) => { + const stepIndex = path.findIndex((key) => key === step); + + if (stepIndex !== -1) { + return path.slice(0, stepIndex + 1); + } + + return [...path, step]; +}; diff --git a/x-pack/plugins/observability_onboarding/public/routes/index.tsx b/x-pack/plugins/observability_onboarding/public/routes/index.tsx index d462844f91d51..0ad61cce116f1 100644 --- a/x-pack/plugins/observability_onboarding/public/routes/index.tsx +++ b/x-pack/plugins/observability_onboarding/public/routes/index.tsx @@ -8,8 +8,8 @@ import * as t from 'io-ts'; import React from 'react'; import { Redirect } from 'react-router-dom'; +import { customLogsRoutes } from '../components/app/custom_logs/wizard'; import { Home } from '../components/app/home'; -import { CustomLogs } from '../components/app/custom_logs'; export type RouteParams = DecodeParams< typeof routes[T]['params'] @@ -26,26 +26,20 @@ export interface Params { path?: t.HasProps; } -export const routes = { +export const baseRoutes = { '/': { - handler: () => { - return ; - }, + handler: () => , params: {}, exact: true, }, '/overview': { - handler: () => { - return ; - }, - params: {}, - exact: true, - }, - '/customLogs': { - handler: () => { - return ; - }, + handler: () => , params: {}, exact: true, }, }; + +export const routes = { + ...baseRoutes, + ...customLogsRoutes, +}; diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx b/x-pack/plugins/observability_onboarding/public/routes/templates/custom_logs.tsx similarity index 79% rename from x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx rename to x-pack/plugins/observability_onboarding/public/routes/templates/custom_logs.tsx index e178771ca5ed0..c8d40c0b22a04 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/index.tsx +++ b/x-pack/plugins/observability_onboarding/public/routes/templates/custom_logs.tsx @@ -9,33 +9,37 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; import React, { ComponentType, useRef, useState } from 'react'; -import { breadcrumbsApp } from '../../../application/app'; +import { breadcrumbsApp } from '../../application/app'; +import { HorizontalSteps } from '../../components/app/custom_logs/wizard/horizontal_steps'; +import { Provider as WizardProvider } from '../../components/app/custom_logs/wizard'; import { FilmstripFrame, FilmstripTransition, TransitionState, -} from '../../shared/filmstrip_transition'; -import { Provider as WizardProvider, Step as WizardStep } from './wizard'; -import { HorizontalSteps } from './wizard/horizontal_steps'; +} from '../../components/shared/filmstrip_transition'; -export function CustomLogs() { +interface Props { + children: React.ReactNode; +} + +export function CustomLogs({ children }: Props) { useBreadcrumbs( [ { text: i18n.translate( - 'xpack.observability_onboarding.breadcrumbs.logs', - { defaultMessage: 'Logs' } + 'xpack.observability_onboarding.breadcrumbs.customLogs', + { defaultMessage: 'Custom Logs' } ), }, ], breadcrumbsApp ); - return ; + return {children}; } const TRANSITION_DURATION = 180; -function AnimatedTransitionsWizard() { +function AnimatedTransitionsWizard({ children }: Props) { const [transition, setTransition] = useState('ready'); const TransitionComponent = useRef(() => null); @@ -89,9 +93,7 @@ function AnimatedTransitionsWizard() { transition === 'back' ? : null } - - - + {children} { // eslint-disable-next-line react/jsx-pascal-case diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 1ee0e0d423c4b..0a99b797662aa 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -39315,7 +39315,6 @@ "xpack.features.ossFeatures.visualizeShortUrlSubFeatureName": "URL courtes", "xpack.features.savedObjectsManagementFeatureName": "Gestion des objets enregistrés", "xpack.features.visualizeFeatureName": "Bibliothèque Visualize", - "xpack.observability_onboarding.breadcrumbs.logs": "Logs", "xpack.observability_onboarding.breadcrumbs.onboarding": "Intégration", "xpack.observability_onboarding.fetcher.error.status": "Erreur", "xpack.observability_onboarding.fetcher.error.title": "Erreur lors de la récupération des ressources", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bcec17f1c71cc..d8ac3978fb484 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -39289,7 +39289,6 @@ "xpack.features.ossFeatures.visualizeShortUrlSubFeatureName": "短い URL", "xpack.features.savedObjectsManagementFeatureName": "保存されたオブジェクトの管理", "xpack.features.visualizeFeatureName": "Visualizeライブラリ", - "xpack.observability_onboarding.breadcrumbs.logs": "ログ", "xpack.observability_onboarding.breadcrumbs.onboarding": "オンボーディング", "xpack.observability_onboarding.fetcher.error.status": "エラー", "xpack.observability_onboarding.fetcher.error.title": "リソースの取得中にエラーが発生しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e1d656e243918..4dee3ed99a491 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -39283,7 +39283,6 @@ "xpack.features.ossFeatures.visualizeShortUrlSubFeatureName": "短 URL", "xpack.features.savedObjectsManagementFeatureName": "已保存对象管理", "xpack.features.visualizeFeatureName": "Visualize 库", - "xpack.observability_onboarding.breadcrumbs.logs": "日志", "xpack.observability_onboarding.breadcrumbs.onboarding": "载入", "xpack.observability_onboarding.fetcher.error.status": "错误", "xpack.observability_onboarding.fetcher.error.title": "提取资源时出错",