Skip to content

Commit

Permalink
[8.17] [Logs UI] Allow editing of non-resolving log views (#210633) (#…
Browse files Browse the repository at this point in the history
…211242)

# Backport

This will backport the following commits from `8.x` to `8.17`:
- [[Logs UI] Allow editing of non-resolving log views
(#210633)](#210633)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Felix
Stürmer","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-02-14T14:55:17Z","message":"[Logs
UI] Allow editing of non-resolving log views (#210633)\n\nThis changes
the settings page of the Logs UI such that it allows editing of log view
settings even if the resolution or status check failed. This allows
recovery from various situations that were previously only recoverable
by resetting the log view
completely.","sha":"41cd657811bd1c00ac15860268c018d4d80085ac","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","Feature:Logs
UI","backport:skip","Team:obs-ux-logs","v8.18.0","v8.17.3"],"title":"[Logs
UI] Allow editing of non-resolving log
views","number":210633,"url":"https://github.com/elastic/kibana/pull/210633","mergeCommit":{"message":"[Logs
UI] Allow editing of non-resolving log views (#210633)\n\nThis changes
the settings page of the Logs UI such that it allows editing of log view
settings even if the resolution or status check failed. This allows
recovery from various situations that were previously only recoverable
by resetting the log view
completely.","sha":"41cd657811bd1c00ac15860268c018d4d80085ac"}},"sourceBranch":"8.x","suggestedTargetBranches":["8.18","8.17"],"targetPullRequestStates":[{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.17","label":"v8.17.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
  • Loading branch information
weltenwort authored Feb 15, 2025
1 parent b7cd76e commit 4de5655
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type IndicesConfigurationPanelProps = PropsOf<typeof IndicesConfigurationPanel>;

type IndicesConfigurationPanelStoryArgs = Pick<
IndicesConfigurationPanelProps,
'isLoading' | 'isReadOnly'
'isLoading' | 'isReadOnly' | 'logViewStatus'
> & {
availableIndexPatterns: MockIndexPatternSpec[];
logIndices: LogIndicesFormState;
Expand All @@ -69,6 +69,7 @@ const IndicesConfigurationPanelTemplate: Story<IndicesConfigurationPanelStoryArg
isLoading,
isReadOnly,
logIndices,
logViewStatus,
}) => {
const logIndicesFormElement = useLogIndicesFormElement(logIndices);

Expand All @@ -78,6 +79,7 @@ const IndicesConfigurationPanelTemplate: Story<IndicesConfigurationPanelStoryArg
isLoading={isLoading}
isReadOnly={isReadOnly}
indicesFormElement={logIndicesFormElement}
logViewStatus={logViewStatus}
/>
<EuiCodeBlock language="json">
// field states{'\n'}
Expand All @@ -103,6 +105,10 @@ const defaultArgs: IndicesConfigurationPanelStoryArgs = {
type: 'index_name' as const,
indexName: 'logs-*',
},
logViewStatus: {
index: 'missing',
reason: 'remoteClusterNotFound',
},
availableIndexPatterns: [
{
id: 'INDEX_PATTERN_A',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import { EuiCheckableCard, EuiFormFieldset, EuiSpacer, EuiTitle } from '@elastic
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback, useEffect, useState } from 'react';
import { useUiTracker } from '@kbn/observability-shared-plugin/public';
import type {
LogDataViewReference,
LogIndexReference,
LogViewStatus,
} from '@kbn/logs-shared-plugin/common';
import {
logIndexNameReferenceRT,
LogDataViewReference,
logDataViewReferenceRT,
LogIndexReference,
logSourcesKibanaAdvancedSettingRT,
} from '@kbn/logs-shared-plugin/common';
import { EuiCallOut } from '@elastic/eui';
Expand All @@ -34,7 +37,8 @@ export const IndicesConfigurationPanel = React.memo<{
isLoading: boolean;
isReadOnly: boolean;
indicesFormElement: FormElement<LogIndexReference | undefined, FormValidationError>;
}>(({ isLoading, isReadOnly, indicesFormElement }) => {
logViewStatus: LogViewStatus;
}>(({ isLoading, isReadOnly, indicesFormElement, logViewStatus }) => {
const {
services: {
http,
Expand Down Expand Up @@ -198,6 +202,7 @@ export const IndicesConfigurationPanel = React.memo<{
/>
)}
</EuiCheckableCard>
<LogViewStatusWarning logViewStatus={logViewStatus} />
{numberOfLogsRules > 0 && indicesFormElement.isDirty && (
<>
<EuiSpacer size="s" />
Expand Down Expand Up @@ -232,6 +237,36 @@ export const IndicesConfigurationPanel = React.memo<{
);
});

const LogViewStatusWarning: React.FC<{ logViewStatus: LogViewStatus }> = ({ logViewStatus }) => {
if (logViewStatus.index === 'missing') {
return (
<>
<EuiSpacer size="s" />
<EuiCallOut title={logIndicesMissingTitle} color="warning" iconType="warning">
{logViewStatus.reason === 'noShardsFound' ? (
<FormattedMessage
id="xpack.infra.sourceConfiguration.logIndicesMissingMessage.noShardsFound"
defaultMessage="No shards found for the specified indices."
/>
) : logViewStatus.reason === 'noIndicesFound' ? (
<FormattedMessage
id="xpack.infra.sourceConfiguration.logIndicesMissingMessage.noIndicesFound"
defaultMessage="No indices found for the specified pattern."
/>
) : logViewStatus.reason === 'remoteClusterNotFound' ? (
<FormattedMessage
id="xpack.infra.sourceConfiguration.logIndicesMissingMessage.remoteClusterNotFound"
defaultMessage="At least one remote cluster was not found."
/>
) : null}
</EuiCallOut>
</>
);
} else {
return null;
}
};

const isDataViewFormElement = isFormElementForType(
(value): value is LogDataViewReference | undefined =>
value == null || logDataViewReferenceRT.is(value)
Expand All @@ -242,3 +277,10 @@ const isIndexNamesFormElement = isFormElementForType(logIndexNameReferenceRT.is)
const isKibanaAdvancedSettingFormElement = isFormElementForType(
logSourcesKibanaAdvancedSettingRT.is
);

const logIndicesMissingTitle = i18n.translate(
'xpack.infra.sourceConfiguration.logIndicesMissingTitle',
{
defaultMessage: 'Log indices missing',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
import { Prompt } from '@kbn/observability-shared-plugin/public';
import { useTrackPageview } from '@kbn/observability-shared-plugin/public';
import { useLogViewContext } from '@kbn/logs-shared-plugin/public';
import type { LogView, LogViewAttributes, LogViewStatus } from '@kbn/logs-shared-plugin/common';
import { SourceErrorPage } from '../../../components/source_error_page';
import { LogsDeprecationCallout } from '../../../components/logs_deprecation_callout';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs';
Expand Down Expand Up @@ -50,21 +52,65 @@ export const LogsSettingsPage = () => {
]);

const {
logView,
hasFailedLoadingLogView,
isInlineLogView,
isLoading,
isUninitialized,
update,
latestLoadLogViewFailures,
logView,
logViewStatus,
resolvedLogView,
isInlineLogView,
retry,
revertToDefaultLogView,
update,
} = useLogViewContext();

const availableFields = useMemo(
() => resolvedLogView?.fields.map((field) => field.name) ?? [],
[resolvedLogView]
);

const isWriteable = shouldAllowEdit && logView != null && logView.origin !== 'internal';

if ((isLoading || isUninitialized) && logView == null) {
return <SourceLoadingPage />;
} else if (hasFailedLoadingLogView || logView == null) {
return <SourceErrorPage errorMessage={latestLoadLogViewFailures[0].message} retry={retry} />;
} else {
return (
<LogsSettingsPageContent
availableFields={availableFields}
isInlineLogView={isInlineLogView}
isLoading={isLoading}
isWriteable={isWriteable}
logView={logView}
logViewStatus={logViewStatus}
revertToDefaultLogView={revertToDefaultLogView}
onUpdateLogViewAttributes={update}
/>
);
}
};

const LogsSettingsPageContent = ({
availableFields,
isInlineLogView,
isLoading,
isWriteable,
logView,
logViewStatus,
revertToDefaultLogView,
onUpdateLogViewAttributes,
}: {
availableFields: string[];
isInlineLogView: boolean;
isLoading: boolean;
isWriteable: boolean;
logView: LogView;
logViewStatus: LogViewStatus;
revertToDefaultLogView: () => void;
onUpdateLogViewAttributes: (logViewAttributes: Partial<LogViewAttributes>) => Promise<void>;
}) => {
const {
sourceConfigurationFormElement,
formState,
Expand All @@ -74,21 +120,15 @@ export const LogsSettingsPage = () => {
} = useLogSourceConfigurationFormState(logView?.attributes);

const persistUpdates = useCallback(async () => {
await update(formState);
sourceConfigurationFormElement.resetValue();
}, [update, sourceConfigurationFormElement, formState]);

const isWriteable = useMemo(
() => shouldAllowEdit && logView && logView.origin !== 'internal',
[shouldAllowEdit, logView]
);

if ((isLoading || isUninitialized) && !resolvedLogView) {
return <SourceLoadingPage />;
}
if (hasFailedLoadingLogView) {
return null;
}
try {
await onUpdateLogViewAttributes(formState);
sourceConfigurationFormElement.resetValue();
} catch {
// the error is handled in the state machine already, but without this the
// global promise rejection tracker would complain about it being
// unhandled
}
}, [onUpdateLogViewAttributes, sourceConfigurationFormElement, formState]);

return (
<EuiErrorBoundary>
Expand Down Expand Up @@ -124,6 +164,7 @@ export const LogsSettingsPage = () => {
isLoading={isLoading}
isReadOnly={!isWriteable}
indicesFormElement={logIndicesFormElement}
logViewStatus={logViewStatus}
/>
</EuiPanel>
<EuiSpacer />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe('Logs UI Observability Homepage Functions', () => {
setup();

getResolvedLogView.mockResolvedValue(createResolvedLogViewMock({ indices: 'test-index' }));
getResolvedLogViewStatus.mockResolvedValue({ index: 'missing' });
getResolvedLogViewStatus.mockResolvedValue({ index: 'missing', reason: 'noShardsFound' });

const hasData = getLogsHasDataFetcher(mockedGetStartServices);
const response = await hasData();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export {
FetchLogViewError,
FetchLogViewStatusError,
ResolveLogViewError,
isNoSuchRemoteClusterError,
} from './log_views/errors';

// eslint-disable-next-line @kbn/eslint/no_export_all
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ export class PutLogViewError extends Error {
this.name = 'PutLogViewError';
}
}

export const isNoSuchRemoteClusterError = (err: Error) =>
err?.message?.includes('no_such_remote_cluster_exception');
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,16 @@ const resolveLegacyReference = async (
}

const indices = logViewAttributes.logIndices.indexName;
const dataViewId = `log-view-${logViewId}`;

// If we didn't remove the item from the cache here the subsequent call to
// create would not have any effect
dataViewsService.clearInstanceCache(dataViewId);

const dataViewReference = await dataViewsService
.create(
{
id: `log-view-${logViewId}`,
id: dataViewId,
name: logViewAttributes.name,
title: indices,
timeFieldName: TIMESTAMP_FIELD,
Expand Down Expand Up @@ -134,11 +139,16 @@ const resolveKibanaAdvancedSettingReference = async (
const indices = (await logSourcesService.getLogSources())
.map((logSource) => logSource.indexPattern)
.join(',');
const dataViewId = `log-view-${logViewId}`;

// If we didn't remove the item from the cache here the subsequent call to
// create would not have any effect
dataViewsService.clearInstanceCache(dataViewId);

const dataViewReference = await dataViewsService
.create(
{
id: `log-view-${logViewId}`,
id: dataViewId,
name: logViewAttributes.name,
title: indices,
timeFieldName: TIMESTAMP_FIELD,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,25 @@ export const logViewRT = rt.exact(
);
export type LogView = rt.TypeOf<typeof logViewRT>;

export const logViewIndexStatusRT = rt.keyof({
available: null,
empty: null,
missing: null,
unknown: null,
});
export type LogViewIndexStatus = rt.TypeOf<typeof logViewIndexStatusRT>;

export const logViewStatusRT = rt.strict({
index: logViewIndexStatusRT,
});
export const logViewStatusRT = rt.union([
rt.strict({
index: rt.literal('available'),
}),
rt.strict({
index: rt.literal('empty'),
}),
rt.strict({
index: rt.literal('missing'),
reason: rt.keyof({
noIndicesFound: null,
noShardsFound: null,
remoteClusterNotFound: null,
}),
}),
rt.strict({
index: rt.literal('unknown'),
}),
]);
export type LogViewStatus = rt.TypeOf<typeof logViewStatusRT>;

export const persistedLogViewReferenceRT = rt.type({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { useInterpret, useSelector } from '@xstate/react';
import createContainer from 'constate';
import { useCallback, useState } from 'react';
import { waitFor } from 'xstate/lib/waitFor';
import { DEFAULT_LOG_VIEW, LogViewAttributes, LogViewReference } from '../../common/log_views';
import {
DEFAULT_LOG_VIEW,
LogViewAttributes,
LogViewReference,
LogViewStatus,
} from '../../common/log_views';
import {
InitializeFromUrl,
UpdateContextInUrl,
Expand Down Expand Up @@ -73,7 +78,10 @@ export const useLogView = ({

const logView = useSelector(logViewStateService, (state) =>
state.matches('resolving') ||
state.matches('updating') ||
state.matches('checkingStatus') ||
state.matches('resolutionFailed') ||
state.matches('checkingStatusFailed') ||
state.matches('resolvedPersistedLogView') ||
state.matches('resolvedInlineLogView')
? state.context.logView
Expand All @@ -82,16 +90,19 @@ export const useLogView = ({

const resolvedLogView = useSelector(logViewStateService, (state) =>
state.matches('checkingStatus') ||
state.matches('checkingStatusFailed') ||
state.matches('resolvedPersistedLogView') ||
state.matches('resolvedInlineLogView')
? state.context.resolvedLogView
: undefined
);

const logViewStatus = useSelector(logViewStateService, (state) =>
state.matches('resolvedPersistedLogView') || state.matches('resolvedInlineLogView')
const logViewStatus: LogViewStatus = useSelector(logViewStateService, (state) =>
state.matches('resolvedPersistedLogView') ||
state.matches('resolvedInlineLogView') ||
state.matches('resolutionFailed')
? state.context.status
: undefined
: { index: 'unknown' }
);

const isLoadingLogView = useSelector(logViewStateService, (state) => state.matches('loading'));
Expand Down
Loading

0 comments on commit 4de5655

Please sign in to comment.