[Lens] Improves lost indexpattern scenario in saved visualization (#91377)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
49aef21bd4
commit
8f6a6e2ae5
|
@ -213,8 +213,8 @@ export const getOutputSubscription = ({
|
|||
}),
|
||||
distinctUntilChanged((a, b) =>
|
||||
deepEqual(
|
||||
a.map((ip) => ip.id),
|
||||
b.map((ip) => ip.id)
|
||||
a.map((ip) => ip && ip.id),
|
||||
b.map((ip) => ip && ip.id)
|
||||
)
|
||||
),
|
||||
// using switchMap for previous task cancellation
|
||||
|
|
|
@ -10,7 +10,7 @@ import './app.scss';
|
|||
import _ from 'lodash';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { NotificationsStart, Toast } from 'kibana/public';
|
||||
import { Toast } from 'kibana/public';
|
||||
import { VisualizeFieldContext } from 'src/plugins/ui_actions/public';
|
||||
import { Datatable } from 'src/plugins/expressions/public';
|
||||
import { EuiBreadcrumb } from '@elastic/eui';
|
||||
|
@ -334,12 +334,11 @@ export function App({
|
|||
initialInput.savedObjectId
|
||||
);
|
||||
}
|
||||
getAllIndexPatterns(
|
||||
_.uniq(doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)),
|
||||
data.indexPatterns,
|
||||
notifications
|
||||
)
|
||||
.then((indexPatterns) => {
|
||||
const indexPatternIds = _.uniq(
|
||||
doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
|
||||
);
|
||||
getAllIndexPatterns(indexPatternIds, data.indexPatterns)
|
||||
.then(({ indexPatterns }) => {
|
||||
// Don't overwrite any pinned filters
|
||||
data.query.filterManager.setAppFilters(
|
||||
injectFilterReferences(doc.state.filters, doc.references)
|
||||
|
@ -683,7 +682,6 @@ export function App({
|
|||
initialContext={initialContext}
|
||||
setState={setState}
|
||||
data={data}
|
||||
notifications={notifications}
|
||||
query={state.query}
|
||||
filters={state.filters}
|
||||
searchSessionId={state.searchSessionId}
|
||||
|
@ -736,7 +734,6 @@ const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({
|
|||
initialContext,
|
||||
setState,
|
||||
data,
|
||||
notifications,
|
||||
lastKnownDoc,
|
||||
activeData: activeDataRef,
|
||||
}: {
|
||||
|
@ -754,7 +751,6 @@ const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({
|
|||
initialContext: VisualizeFieldContext | undefined;
|
||||
setState: React.Dispatch<React.SetStateAction<LensAppState>>;
|
||||
data: DataPublicPluginStart;
|
||||
notifications: NotificationsStart;
|
||||
lastKnownDoc: React.MutableRefObject<Document | undefined>;
|
||||
activeData: React.MutableRefObject<Record<string, Datatable> | undefined>;
|
||||
}) {
|
||||
|
@ -790,8 +786,8 @@ const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({
|
|||
(id) => !indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id)
|
||||
)
|
||||
) {
|
||||
getAllIndexPatterns(filterableIndexPatterns, data.indexPatterns, notifications).then(
|
||||
(indexPatterns) => {
|
||||
getAllIndexPatterns(filterableIndexPatterns, data.indexPatterns).then(
|
||||
({ indexPatterns }) => {
|
||||
if (indexPatterns) {
|
||||
setState((s) => ({ ...s, indexPatternsForTopNav: indexPatterns }));
|
||||
}
|
||||
|
@ -806,18 +802,16 @@ const MemoizedEditorFrameWrapper = React.memo(function EditorFrameWrapper({
|
|||
|
||||
export async function getAllIndexPatterns(
|
||||
ids: string[],
|
||||
indexPatternsService: IndexPatternsContract,
|
||||
notifications: NotificationsStart
|
||||
): Promise<IndexPatternInstance[]> {
|
||||
try {
|
||||
return await Promise.all(ids.map((id) => indexPatternsService.get(id)));
|
||||
} catch (e) {
|
||||
notifications.toasts.addDanger(
|
||||
i18n.translate('xpack.lens.app.indexPatternLoadingError', {
|
||||
defaultMessage: 'Error loading index patterns',
|
||||
})
|
||||
);
|
||||
|
||||
throw new Error(e);
|
||||
}
|
||||
indexPatternsService: IndexPatternsContract
|
||||
): Promise<{ indexPatterns: IndexPatternInstance[]; rejectedIds: string[] }> {
|
||||
const responses = await Promise.allSettled(ids.map((id) => indexPatternsService.get(id)));
|
||||
const fullfilled = responses.filter(
|
||||
(response): response is PromiseFulfilledResult<IndexPatternInstance> =>
|
||||
response.status === 'fulfilled'
|
||||
);
|
||||
const rejectedIds = responses
|
||||
.map((_response, i) => ids[i])
|
||||
.filter((id, i) => responses[i].status === 'rejected');
|
||||
// return also the rejected ids in case we want to show something later on
|
||||
return { indexPatterns: fullfilled.map((response) => response.value), rejectedIds };
|
||||
}
|
||||
|
|
|
@ -14,9 +14,16 @@ import { ReactWrapper } from 'enzyme';
|
|||
jest.mock('react-virtualized-auto-sizer', () => {
|
||||
return function (props: {
|
||||
children: (dimensions: { width: number; height: number }) => React.ReactNode;
|
||||
disableHeight?: boolean;
|
||||
}) {
|
||||
const { children, ...otherProps } = props;
|
||||
return <div {...otherProps}>{children({ width: 100, height: 100 })}</div>;
|
||||
const { children, disableHeight, ...otherProps } = props;
|
||||
return (
|
||||
// js-dom may complain that a non-DOM attributes are used when appending props
|
||||
// Handle the disableHeight case using native DOM styling
|
||||
<div {...otherProps} style={disableHeight ? { height: 0 } : {}}>
|
||||
{children({ width: 100, height: 100 })}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -66,7 +73,7 @@ function getDefaultProps() {
|
|||
dateRange: { fromDate: '', toDate: '' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
core: coreMock.createSetup(),
|
||||
core: coreMock.createStart(),
|
||||
plugins: {
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect, useReducer, useState, useCallback } from 'react';
|
||||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
|
||||
import { Datasource, FramePublicAPI, Visualization } from '../../types';
|
||||
|
@ -40,7 +40,7 @@ export interface EditorFrameProps {
|
|||
ExpressionRenderer: ReactExpressionRendererType;
|
||||
palettes: PaletteRegistry;
|
||||
onError: (e: { message: string }) => void;
|
||||
core: CoreSetup | CoreStart;
|
||||
core: CoreStart;
|
||||
plugins: EditorFrameStartPlugins;
|
||||
dateRange: {
|
||||
fromDate: string;
|
||||
|
|
|
@ -20,7 +20,11 @@ import { Document } from '../../persistence/saved_object_store';
|
|||
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
|
||||
import { getActiveDatasourceIdFromDoc } from './state_management';
|
||||
import { ErrorMessage } from '../types';
|
||||
import { getMissingCurrentDatasource, getMissingVisualizationTypeError } from '../error_helper';
|
||||
import {
|
||||
getMissingCurrentDatasource,
|
||||
getMissingIndexPatterns,
|
||||
getMissingVisualizationTypeError,
|
||||
} from '../error_helper';
|
||||
|
||||
export async function initializeDatasources(
|
||||
datasourceMap: Record<string, Datasource>,
|
||||
|
@ -112,6 +116,19 @@ export async function persistedStateToExpression(
|
|||
errors: [{ shortMessage: '', longMessage: getMissingCurrentDatasource() }],
|
||||
};
|
||||
}
|
||||
|
||||
const indexPatternValidation = validateRequiredIndexPatterns(
|
||||
datasources[datasourceId],
|
||||
datasourceStates[datasourceId]
|
||||
);
|
||||
|
||||
if (indexPatternValidation) {
|
||||
return {
|
||||
ast: null,
|
||||
errors: indexPatternValidation,
|
||||
};
|
||||
}
|
||||
|
||||
const validationResult = validateDatasourceAndVisualization(
|
||||
datasources[datasourceId],
|
||||
datasourceStates[datasourceId].state,
|
||||
|
@ -134,6 +151,33 @@ export async function persistedStateToExpression(
|
|||
};
|
||||
}
|
||||
|
||||
export function getMissingIndexPattern(
|
||||
currentDatasource: Datasource | null,
|
||||
currentDatasourceState: { state: unknown } | null
|
||||
) {
|
||||
if (currentDatasourceState == null || currentDatasource == null) {
|
||||
return [];
|
||||
}
|
||||
const missingIds = currentDatasource.checkIntegrity(currentDatasourceState.state);
|
||||
if (!missingIds.length) {
|
||||
return [];
|
||||
}
|
||||
return missingIds;
|
||||
}
|
||||
|
||||
const validateRequiredIndexPatterns = (
|
||||
currentDatasource: Datasource,
|
||||
currentDatasourceState: { state: unknown } | null
|
||||
): ErrorMessage[] | undefined => {
|
||||
const missingIds = getMissingIndexPattern(currentDatasource, currentDatasourceState);
|
||||
|
||||
if (!missingIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return [{ shortMessage: '', longMessage: getMissingIndexPatterns(missingIds), type: 'fixable' }];
|
||||
};
|
||||
|
||||
export const validateDatasourceAndVisualization = (
|
||||
currentDataSource: Datasource | null,
|
||||
currentDatasourceState: unknown | null,
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('editor_frame state management', () => {
|
|||
initialVisualizationId: 'testVis',
|
||||
ExpressionRenderer: createExpressionRendererMock(),
|
||||
onChange: jest.fn(),
|
||||
core: coreMock.createSetup(),
|
||||
core: coreMock.createStart(),
|
||||
plugins: {
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
|
|
|
@ -291,4 +291,24 @@ describe('suggestion_panel', () => {
|
|||
expect(wrapper.find(EuiIcon)).toHaveLength(1);
|
||||
expect(wrapper.find(EuiIcon).prop('type')).toEqual(LensIconChartDatatable);
|
||||
});
|
||||
|
||||
it('should return no suggestion if visualization has missing index-patterns', () => {
|
||||
// create a layer that is referencing an indexPatterns not retrieved by the datasource
|
||||
const missingIndexPatternsState = {
|
||||
layers: { indexPatternId: 'a' },
|
||||
indexPatterns: {},
|
||||
};
|
||||
mockDatasource.checkIntegrity.mockReturnValue(['a']);
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
datasourceStates: {
|
||||
mock: {
|
||||
...defaultProps.datasourceStates.mock,
|
||||
state: missingIndexPatternsState,
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = mount(<SuggestionPanel {...newProps} />);
|
||||
expect(wrapper.html()).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,7 +34,7 @@ import {
|
|||
} from '../../../../../../src/plugins/expressions/public';
|
||||
import { prependDatasourceExpression } from './expression_helpers';
|
||||
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';
|
||||
import { validateDatasourceAndVisualization } from './state_helpers';
|
||||
import { getMissingIndexPattern, validateDatasourceAndVisualization } from './state_helpers';
|
||||
|
||||
const MAX_SUGGESTIONS_DISPLAYED = 5;
|
||||
|
||||
|
@ -182,46 +182,52 @@ export function SuggestionPanel({
|
|||
? stagedPreview.visualization.activeId
|
||||
: activeVisualizationId;
|
||||
|
||||
const missingIndexPatterns = getMissingIndexPattern(
|
||||
activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
|
||||
activeDatasourceId ? datasourceStates[activeDatasourceId] : null
|
||||
);
|
||||
const { suggestions, currentStateExpression, currentStateError } = useMemo(
|
||||
() => {
|
||||
const newSuggestions = getSuggestions({
|
||||
datasourceMap,
|
||||
datasourceStates: currentDatasourceStates,
|
||||
visualizationMap,
|
||||
activeVisualizationId: currentVisualizationId,
|
||||
visualizationState: currentVisualizationState,
|
||||
activeData: frame.activeData,
|
||||
})
|
||||
.filter((suggestion) => !suggestion.hide)
|
||||
.filter(
|
||||
({
|
||||
visualizationId,
|
||||
visualizationState: suggestionVisualizationState,
|
||||
datasourceState: suggestionDatasourceState,
|
||||
datasourceId: suggetionDatasourceId,
|
||||
}) => {
|
||||
return (
|
||||
validateDatasourceAndVisualization(
|
||||
suggetionDatasourceId ? datasourceMap[suggetionDatasourceId] : null,
|
||||
suggestionDatasourceState,
|
||||
visualizationMap[visualizationId],
|
||||
suggestionVisualizationState,
|
||||
frame
|
||||
) == null
|
||||
);
|
||||
}
|
||||
)
|
||||
.slice(0, MAX_SUGGESTIONS_DISPLAYED)
|
||||
.map((suggestion) => ({
|
||||
...suggestion,
|
||||
previewExpression: preparePreviewExpression(
|
||||
suggestion,
|
||||
visualizationMap[suggestion.visualizationId],
|
||||
const newSuggestions = missingIndexPatterns.length
|
||||
? []
|
||||
: getSuggestions({
|
||||
datasourceMap,
|
||||
currentDatasourceStates,
|
||||
frame
|
||||
),
|
||||
}));
|
||||
datasourceStates: currentDatasourceStates,
|
||||
visualizationMap,
|
||||
activeVisualizationId: currentVisualizationId,
|
||||
visualizationState: currentVisualizationState,
|
||||
activeData: frame.activeData,
|
||||
})
|
||||
.filter((suggestion) => !suggestion.hide)
|
||||
.filter(
|
||||
({
|
||||
visualizationId,
|
||||
visualizationState: suggestionVisualizationState,
|
||||
datasourceState: suggestionDatasourceState,
|
||||
datasourceId: suggetionDatasourceId,
|
||||
}) => {
|
||||
return (
|
||||
validateDatasourceAndVisualization(
|
||||
suggetionDatasourceId ? datasourceMap[suggetionDatasourceId] : null,
|
||||
suggestionDatasourceState,
|
||||
visualizationMap[visualizationId],
|
||||
suggestionVisualizationState,
|
||||
frame
|
||||
) == null
|
||||
);
|
||||
}
|
||||
)
|
||||
.slice(0, MAX_SUGGESTIONS_DISPLAYED)
|
||||
.map((suggestion) => ({
|
||||
...suggestion,
|
||||
previewExpression: preparePreviewExpression(
|
||||
suggestion,
|
||||
visualizationMap[suggestion.visualizationId],
|
||||
datasourceMap,
|
||||
currentDatasourceStates,
|
||||
frame
|
||||
),
|
||||
}));
|
||||
|
||||
const validationErrors = validateDatasourceAndVisualization(
|
||||
activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
|
||||
|
|
|
@ -41,6 +41,20 @@ import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/pub
|
|||
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public/embeddable';
|
||||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
|
||||
const defaultPermissions: Record<string, Record<string, boolean | Record<string, boolean>>> = {
|
||||
navLinks: { management: true },
|
||||
management: { kibana: { indexPatterns: true } },
|
||||
};
|
||||
|
||||
function createCoreStartWithPermissions(newCapabilities = defaultPermissions) {
|
||||
const core = coreMock.createStart();
|
||||
((core.application.capabilities as unknown) as Record<
|
||||
string,
|
||||
Record<string, boolean | Record<string, boolean>>
|
||||
>) = newCapabilities;
|
||||
return core;
|
||||
}
|
||||
|
||||
describe('workspace_panel', () => {
|
||||
let mockVisualization: jest.Mocked<Visualization>;
|
||||
let mockVisualization2: jest.Mocked<Visualization>;
|
||||
|
@ -84,7 +98,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -108,7 +122,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -132,7 +146,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -170,7 +184,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -211,7 +225,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -255,7 +269,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={dispatch}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -307,7 +321,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -386,7 +400,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -443,7 +457,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -471,6 +485,130 @@ describe('workspace_panel', () => {
|
|||
expect(expressionRendererMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should show an error message if there are missing indexpatterns in the visualization', () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
mockDatasource.checkIntegrity.mockReturnValue(['a']);
|
||||
const framePublicAPI = createMockFramePublicAPI();
|
||||
framePublicAPI.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
};
|
||||
instance = mount(
|
||||
<WorkspacePanel
|
||||
activeDatasourceId={'mock'}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
// define a layer with an indexpattern not available
|
||||
state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
activeVisualizationId="vis"
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
}}
|
||||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(instance.find('[data-test-subj="missing-refs-failure"]').exists()).toBeTruthy();
|
||||
expect(instance.find(expressionRendererMock)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not show the management action in case of missing indexpattern and no navigation permissions', () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
const framePublicAPI = createMockFramePublicAPI();
|
||||
framePublicAPI.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
};
|
||||
|
||||
instance = mount(
|
||||
<WorkspacePanel
|
||||
activeDatasourceId={'mock'}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
// define a layer with an indexpattern not available
|
||||
state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
activeVisualizationId="vis"
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
}}
|
||||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
// Use cannot navigate to the management page
|
||||
core={createCoreStartWithPermissions({
|
||||
navLinks: { management: false },
|
||||
management: { kibana: { indexPatterns: true } },
|
||||
})}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
instance.find('[data-test-subj="configuration-failure-reconfigure-indexpatterns"]').exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not show the management action in case of missing indexpattern and no indexPattern specific permissions', () => {
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
const framePublicAPI = createMockFramePublicAPI();
|
||||
framePublicAPI.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
};
|
||||
|
||||
instance = mount(
|
||||
<WorkspacePanel
|
||||
activeDatasourceId={'mock'}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
// define a layer with an indexpattern not available
|
||||
state: { layers: { indexPatternId: 'a' }, indexPatterns: {} },
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
activeVisualizationId="vis"
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
}}
|
||||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
// user can go to management, but indexPatterns management is not accessible
|
||||
core={createCoreStartWithPermissions({
|
||||
navLinks: { management: true },
|
||||
management: { kibana: { indexPatterns: false } },
|
||||
})}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
instance.find('[data-test-subj="configuration-failure-reconfigure-indexpatterns"]').exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should show an error message if validation on datasource does not pass', () => {
|
||||
mockDatasource.getErrorMessages.mockReturnValue([
|
||||
{ shortMessage: 'An error occurred', longMessage: 'An long description here' },
|
||||
|
@ -501,7 +639,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -543,7 +681,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -587,7 +725,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -628,7 +766,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -667,7 +805,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -712,7 +850,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={() => undefined}
|
||||
/>
|
||||
|
@ -781,7 +919,7 @@ describe('workspace_panel', () => {
|
|||
visualizationState={{}}
|
||||
dispatch={mockDispatch}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
core={createCoreStartWithPermissions()}
|
||||
plugins={{ uiActions: uiActionsMock, data: dataMock }}
|
||||
getSuggestionForField={mockGetSuggestionForField}
|
||||
/>
|
||||
|
|
|
@ -19,12 +19,13 @@ import {
|
|||
EuiLink,
|
||||
EuiPageContentBody,
|
||||
} from '@elastic/eui';
|
||||
import { CoreStart, CoreSetup } from 'kibana/public';
|
||||
import { CoreStart, ApplicationStart } from 'kibana/public';
|
||||
import {
|
||||
DataPublicPluginStart,
|
||||
ExecutionContextSearch,
|
||||
TimefilterContract,
|
||||
} from 'src/plugins/data/public';
|
||||
import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import {
|
||||
ExpressionRendererEvent,
|
||||
ExpressionRenderError,
|
||||
|
@ -52,7 +53,7 @@ import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualiza
|
|||
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
|
||||
import { DropIllustration } from '../../../assets/drop_illustration';
|
||||
import { getOriginalRequestErrorMessage } from '../../error_helper';
|
||||
import { validateDatasourceAndVisualization } from '../state_helpers';
|
||||
import { getMissingIndexPattern, validateDatasourceAndVisualization } from '../state_helpers';
|
||||
import { DefaultInspectorAdapters } from '../../../../../../../src/plugins/expressions/common';
|
||||
|
||||
export interface WorkspacePanelProps {
|
||||
|
@ -71,7 +72,7 @@ export interface WorkspacePanelProps {
|
|||
framePublicAPI: FramePublicAPI;
|
||||
dispatch: (action: Action) => void;
|
||||
ExpressionRenderer: ReactExpressionRendererType;
|
||||
core: CoreStart | CoreSetup;
|
||||
core: CoreStart;
|
||||
plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart };
|
||||
title?: string;
|
||||
visualizeTriggerFieldContext?: VisualizeFieldContext;
|
||||
|
@ -139,6 +140,27 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
? visualizationMap[activeVisualizationId]
|
||||
: null;
|
||||
|
||||
const missingIndexPatterns = getMissingIndexPattern(
|
||||
activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
|
||||
activeDatasourceId ? datasourceStates[activeDatasourceId] : null
|
||||
);
|
||||
|
||||
const missingRefsErrors = missingIndexPatterns.length
|
||||
? [
|
||||
{
|
||||
shortMessage: '',
|
||||
longMessage: i18n.translate('xpack.lens.indexPattern.missingIndexPattern', {
|
||||
defaultMessage:
|
||||
'The {count, plural, one {index pattern} other {index patterns}} ({count, plural, one {id} other {ids}}: {indexpatterns}) cannot be found',
|
||||
values: {
|
||||
count: missingIndexPatterns.length,
|
||||
indexpatterns: missingIndexPatterns.join(', '),
|
||||
},
|
||||
}),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
// Note: mind to all these eslint disable lines: the frameAPI will change too frequently
|
||||
// and to prevent race conditions it is ok to leave them there.
|
||||
|
||||
|
@ -157,7 +179,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
|
||||
const expression = useMemo(
|
||||
() => {
|
||||
if (!configurationValidationError?.length) {
|
||||
if (!configurationValidationError?.length && !missingRefsErrors.length) {
|
||||
try {
|
||||
const ast = buildExpression({
|
||||
visualization: activeVisualization,
|
||||
|
@ -310,8 +332,9 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
dispatch={dispatch}
|
||||
onEvent={onEvent}
|
||||
setLocalState={setLocalState}
|
||||
localState={{ ...localState, configurationValidationError }}
|
||||
localState={{ ...localState, configurationValidationError, missingRefsErrors }}
|
||||
ExpressionRendererComponent={ExpressionRendererComponent}
|
||||
application={core.application}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -354,6 +377,7 @@ export const InnerVisualizationWrapper = ({
|
|||
localState,
|
||||
ExpressionRendererComponent,
|
||||
dispatch,
|
||||
application,
|
||||
}: {
|
||||
expression: string | null | undefined;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
|
@ -363,8 +387,10 @@ export const InnerVisualizationWrapper = ({
|
|||
setLocalState: (dispatch: (prevState: WorkspaceState) => WorkspaceState) => void;
|
||||
localState: WorkspaceState & {
|
||||
configurationValidationError?: Array<{ shortMessage: string; longMessage: string }>;
|
||||
missingRefsErrors?: Array<{ shortMessage: string; longMessage: string }>;
|
||||
};
|
||||
ExpressionRendererComponent: ReactExpressionRendererType;
|
||||
application: ApplicationStart;
|
||||
}) => {
|
||||
const context: ExecutionContextSearch = useMemo(
|
||||
() => ({
|
||||
|
@ -454,6 +480,52 @@ export const InnerVisualizationWrapper = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (localState.missingRefsErrors?.length) {
|
||||
// Check for access to both Management app && specific indexPattern section
|
||||
const { management: isManagementEnabled } = application.capabilities.navLinks;
|
||||
const isIndexPatternManagementEnabled =
|
||||
application.capabilities.management.kibana.indexPatterns;
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="configuration-failure">
|
||||
<EuiFlexItem>
|
||||
<EuiEmptyPrompt
|
||||
actions={
|
||||
isManagementEnabled && isIndexPatternManagementEnabled ? (
|
||||
<RedirectAppLinks application={application}>
|
||||
<a
|
||||
href={application.getUrlForApp('management', {
|
||||
path: '/kibana/indexPatterns/create',
|
||||
})}
|
||||
data-test-subj="configuration-failure-reconfigure-indexpatterns"
|
||||
>
|
||||
{i18n.translate('xpack.lens.editorFrame.indexPatternReconfigure', {
|
||||
defaultMessage: `Recreate it in the index pattern management page`,
|
||||
})}
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
) : null
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p className="eui-textBreakAll" data-test-subj="missing-refs-failure">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.editorFrame.indexPatternNotFound"
|
||||
defaultMessage="Index pattern not found"
|
||||
/>
|
||||
</p>
|
||||
<p className="eui-textBreakWord lnsSelectableErrorMessage">
|
||||
{localState.missingRefsErrors[0].longMessage}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
iconColor="danger"
|
||||
iconType="alert"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (localState.expressionBuildError?.length) {
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
|
|
|
@ -530,7 +530,7 @@ describe('embeddable', () => {
|
|||
attributeService,
|
||||
expressionRenderer,
|
||||
basePath,
|
||||
indexPatternService: {} as IndexPatternsContract,
|
||||
indexPatternService: ({ get: jest.fn() } as unknown) as IndexPatternsContract,
|
||||
editable: true,
|
||||
getTrigger,
|
||||
documentToExpression: () =>
|
||||
|
|
|
@ -296,6 +296,7 @@ export class Embeddable
|
|||
hasCompatibleActions={this.hasCompatibleActions}
|
||||
className={input.className}
|
||||
style={input.style}
|
||||
canEdit={this.deps.editable && input.viewMode === 'edit'}
|
||||
/>,
|
||||
domNode
|
||||
);
|
||||
|
@ -388,22 +389,19 @@ export class Embeddable
|
|||
if (!this.savedVis) {
|
||||
return;
|
||||
}
|
||||
const promises = _.uniqBy(
|
||||
this.savedVis.references.filter(({ type }) => type === 'index-pattern'),
|
||||
'id'
|
||||
)
|
||||
.map(async ({ id }) => {
|
||||
try {
|
||||
return await this.deps.indexPatternService.get(id);
|
||||
} catch (error) {
|
||||
// Unable to load index pattern, ignore error as the index patterns are only used to
|
||||
// configure the filter and query bar - there is still a good chance to get the visualization
|
||||
// to show.
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((promise): promise is Promise<IndexPattern> => Boolean(promise));
|
||||
const indexPatterns = await Promise.all(promises);
|
||||
const responses = await Promise.allSettled(
|
||||
_.uniqBy(
|
||||
this.savedVis.references.filter(({ type }) => type === 'index-pattern'),
|
||||
'id'
|
||||
).map(({ id }) => this.deps.indexPatternService.get(id))
|
||||
);
|
||||
const indexPatterns = responses
|
||||
.filter(
|
||||
(response): response is PromiseFulfilledResult<IndexPattern> =>
|
||||
response.status === 'fulfilled'
|
||||
)
|
||||
.map(({ value }) => value);
|
||||
|
||||
// passing edit url and index patterns to the output of this embeddable for
|
||||
// the container to pick them up and use them to configure filter bar and
|
||||
// config dropdown correctly.
|
||||
|
|
|
@ -37,13 +37,17 @@ export interface ExpressionWrapperProps {
|
|||
hasCompatibleActions?: ReactExpressionRendererProps['hasCompatibleActions'];
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
interface VisualizationErrorProps {
|
||||
errors: ExpressionWrapperProps['errors'];
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
export function VisualizationErrorPanel({ errors }: VisualizationErrorProps) {
|
||||
export function VisualizationErrorPanel({ errors, canEdit }: VisualizationErrorProps) {
|
||||
const showMore = errors && errors.length > 1;
|
||||
const canFixInLens = canEdit && errors?.some(({ type }) => type === 'fixable');
|
||||
return (
|
||||
<div className="lnsEmbeddedError">
|
||||
<EuiEmptyPrompt
|
||||
|
@ -55,7 +59,7 @@ export function VisualizationErrorPanel({ errors }: VisualizationErrorProps) {
|
|||
{errors ? (
|
||||
<>
|
||||
<p>{errors[0].longMessage}</p>
|
||||
{errors.length > 1 ? (
|
||||
{showMore && !canFixInLens ? (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.embeddable.moreErrors"
|
||||
|
@ -63,6 +67,14 @@ export function VisualizationErrorPanel({ errors }: VisualizationErrorProps) {
|
|||
/>
|
||||
</p>
|
||||
) : null}
|
||||
{canFixInLens ? (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.embeddable.fixErrors"
|
||||
defaultMessage="Edit in Lens editor to fix the error"
|
||||
/>
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
|
@ -93,11 +105,12 @@ export function ExpressionWrapper({
|
|||
style,
|
||||
className,
|
||||
errors,
|
||||
canEdit,
|
||||
}: ExpressionWrapperProps) {
|
||||
return (
|
||||
<I18nProvider>
|
||||
{errors || expression === null || expression === '' ? (
|
||||
<VisualizationErrorPanel errors={errors} />
|
||||
<VisualizationErrorPanel errors={errors} canEdit={canEdit} />
|
||||
) : (
|
||||
<div className={classNames('lnsExpressionRenderer', className)} style={style}>
|
||||
<ExpressionRendererComponent
|
||||
|
|
|
@ -87,3 +87,11 @@ export function getMissingCurrentDatasource() {
|
|||
defaultMessage: 'Could not find datasource for the visualization',
|
||||
});
|
||||
}
|
||||
|
||||
export function getMissingIndexPatterns(indexPatternIds: string[]) {
|
||||
return i18n.translate('xpack.lens.editorFrame.expressionMissingIndexPattern', {
|
||||
defaultMessage:
|
||||
'Could not find the {count, plural, one {index pattern} other {index pattern}}: {ids}',
|
||||
values: { count: indexPatternIds.length, ids: indexPatternIds.join(', ') },
|
||||
});
|
||||
}
|
||||
|
|
|
@ -97,6 +97,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
// but can be used to validate whether specific API mock functions are called
|
||||
publicAPIMock,
|
||||
getErrorMessages: jest.fn((_state) => undefined),
|
||||
checkIntegrity: jest.fn((_state) => []),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -12,4 +12,5 @@ export type TableInspectorAdapter = Record<string, Datatable>;
|
|||
export interface ErrorMessage {
|
||||
shortMessage: string;
|
||||
longMessage: string;
|
||||
type?: 'fixable' | 'critical';
|
||||
}
|
||||
|
|
|
@ -32,6 +32,15 @@ export function ChangeIndexPattern({
|
|||
}) {
|
||||
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
|
||||
|
||||
const isMissingCurrent = !indexPatternRefs.some(({ id }) => id === indexPatternId);
|
||||
|
||||
// be careful to only add color with a value, otherwise it will fallbacks to "primary"
|
||||
const colorProp = isMissingCurrent
|
||||
? {
|
||||
color: 'danger' as const,
|
||||
}
|
||||
: {};
|
||||
|
||||
const createTrigger = function () {
|
||||
const { label, title, ...rest } = trigger;
|
||||
return (
|
||||
|
@ -39,6 +48,7 @@ export function ChangeIndexPattern({
|
|||
title={title}
|
||||
onClick={() => setPopoverIsOpen(!isPopoverOpen)}
|
||||
fullWidth
|
||||
{...colorProp}
|
||||
{...rest}
|
||||
>
|
||||
{label}
|
||||
|
|
|
@ -146,8 +146,8 @@ export function IndexPatternDataPanel({
|
|||
.map((l) => l.indexPatternId)
|
||||
.concat(currentIndexPatternId)
|
||||
)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.filter((id) => !!indexPatterns[id])
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((id) => ({
|
||||
id,
|
||||
title: indexPatterns[id].title,
|
||||
|
@ -171,7 +171,7 @@ export function IndexPatternDataPanel({
|
|||
dateRange,
|
||||
setState,
|
||||
isFirstExistenceFetch: state.isFirstExistenceFetch,
|
||||
currentIndexPatternTitle: indexPatterns[currentIndexPatternId].title,
|
||||
currentIndexPatternTitle: indexPatterns[currentIndexPatternId]?.title || '',
|
||||
showNoDataPopover,
|
||||
indexPatterns: indexPatternList,
|
||||
fetchJson: core.http.post,
|
||||
|
@ -188,7 +188,7 @@ export function IndexPatternDataPanel({
|
|||
]}
|
||||
/>
|
||||
|
||||
{Object.keys(indexPatterns).length === 0 ? (
|
||||
{Object.keys(indexPatterns).length === 0 && indexPatternRefs.length === 0 ? (
|
||||
<EuiFlexGroup
|
||||
gutterSize="m"
|
||||
className="lnsInnerIndexPatternDataPanel"
|
||||
|
|
|
@ -416,6 +416,10 @@ export function getIndexPatternDatasource({
|
|||
});
|
||||
return messages.length ? messages : undefined;
|
||||
},
|
||||
checkIntegrity: (state) => {
|
||||
const ids = Object.values(state.layers || {}).map(({ indexPatternId }) => indexPatternId);
|
||||
return ids.filter((id) => !state.indexPatterns[id]);
|
||||
},
|
||||
};
|
||||
|
||||
return indexPatternDatasource;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DatasourceLayerPanelProps } from '../types';
|
||||
import { IndexPatternPrivateState } from './types';
|
||||
import { ChangeIndexPattern } from './change_indexpattern';
|
||||
|
@ -20,13 +21,19 @@ export interface IndexPatternLayerPanelProps
|
|||
export function LayerPanel({ state, layerId, onChangeIndexPattern }: IndexPatternLayerPanelProps) {
|
||||
const layer = state.layers[layerId];
|
||||
|
||||
const indexPattern = state.indexPatterns[layer.indexPatternId];
|
||||
|
||||
const notFoundTitleLabel = i18n.translate('xpack.lens.layerPanel.missingIndexPattern', {
|
||||
defaultMessage: 'Index pattern not found',
|
||||
});
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<ChangeIndexPattern
|
||||
data-test-subj="indexPattern-switcher"
|
||||
trigger={{
|
||||
label: state.indexPatterns[layer.indexPatternId].title,
|
||||
title: state.indexPatterns[layer.indexPatternId].title,
|
||||
label: indexPattern?.title || notFoundTitleLabel,
|
||||
title: indexPattern?.title || notFoundTitleLabel,
|
||||
'data-test-subj': 'lns_layerIndexPatternLabel',
|
||||
size: 's',
|
||||
fontWeight: 'normal',
|
||||
|
|
|
@ -186,7 +186,11 @@ const sampleIndexPatterns = {
|
|||
function mockIndexPatternsService() {
|
||||
return ({
|
||||
get: jest.fn(async (id: '1' | '2') => {
|
||||
return { ...sampleIndexPatternsFromService[id], metaFields: [] };
|
||||
const result = { ...sampleIndexPatternsFromService[id], metaFields: [] };
|
||||
if (!result.fields) {
|
||||
result.fields = [];
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
getIdsWithTitle: jest.fn(async () => {
|
||||
return [
|
||||
|
@ -691,7 +695,7 @@ describe('loader', () => {
|
|||
|
||||
expect(setState).not.toHaveBeenCalled();
|
||||
expect(storage.set).not.toHaveBeenCalled();
|
||||
expect(onError).toHaveBeenCalledWith(err);
|
||||
expect(onError).toHaveBeenCalledWith(Error('Missing indexpatterns'));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -819,7 +823,7 @@ describe('loader', () => {
|
|||
|
||||
expect(setState).not.toHaveBeenCalled();
|
||||
expect(storage.set).not.toHaveBeenCalled();
|
||||
expect(onError).toHaveBeenCalledWith(err);
|
||||
expect(onError).toHaveBeenCalledWith(Error('Missing indexpatterns'));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import { DateRange, ExistingFields } from '../../common/types';
|
|||
import { BASE_API_URL } from '../../common';
|
||||
import {
|
||||
IndexPatternsContract,
|
||||
IndexPattern as IndexPatternInstance,
|
||||
indexPatterns as indexPatternsUtils,
|
||||
} from '../../../../../src/plugins/data/public';
|
||||
import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public';
|
||||
|
@ -48,7 +49,17 @@ export async function loadIndexPatterns({
|
|||
return cache;
|
||||
}
|
||||
|
||||
const indexPatterns = await Promise.all(missingIds.map((id) => indexPatternsService.get(id)));
|
||||
const allIndexPatterns = await Promise.allSettled(
|
||||
missingIds.map((id) => indexPatternsService.get(id))
|
||||
);
|
||||
// ignore rejected indexpatterns here, they're already handled at the app level
|
||||
const indexPatterns = allIndexPatterns
|
||||
.filter(
|
||||
(response): response is PromiseFulfilledResult<IndexPatternInstance> =>
|
||||
response.status === 'fulfilled'
|
||||
)
|
||||
.map((response) => response.value);
|
||||
|
||||
const indexPatternsObject = indexPatterns.reduce(
|
||||
(acc, indexPattern) => {
|
||||
const newFields = indexPattern.fields
|
||||
|
@ -201,13 +212,16 @@ export async function loadInitialState({
|
|||
options?: InitializationOptions;
|
||||
}): Promise<IndexPatternPrivateState> {
|
||||
const { isFullEditor } = options ?? {};
|
||||
const indexPatternRefs = await (isFullEditor ? loadIndexPatternRefs(indexPatternsService) : []);
|
||||
// make it explicit or TS will infer never[] and break few lines down
|
||||
const indexPatternRefs: IndexPatternRef[] = await (isFullEditor
|
||||
? loadIndexPatternRefs(indexPatternsService)
|
||||
: []);
|
||||
const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs);
|
||||
|
||||
const state =
|
||||
persistedState && references ? injectReferences(persistedState, references) : undefined;
|
||||
|
||||
const requiredPatterns = _.uniq(
|
||||
const requiredPatterns: string[] = _.uniq(
|
||||
state
|
||||
? Object.values(state.layers)
|
||||
.map((l) => l.indexPatternId)
|
||||
|
@ -217,11 +231,26 @@ export async function loadInitialState({
|
|||
// take out the undefined from the list
|
||||
.filter(Boolean);
|
||||
|
||||
const currentIndexPatternId = initialContext?.indexPatternId ?? requiredPatterns[0];
|
||||
const availableIndexPatterns = new Set(indexPatternRefs.map(({ id }: IndexPatternRef) => id));
|
||||
// Priority list:
|
||||
// * start with the indexPattern in context
|
||||
// * then fallback to the required ones
|
||||
// * then as last resort use a random one from the available list
|
||||
const availableIndexPatternIds = [
|
||||
initialContext?.indexPatternId,
|
||||
...requiredPatterns,
|
||||
indexPatternRefs[0]?.id,
|
||||
].filter((id) => id != null && availableIndexPatterns.has(id));
|
||||
|
||||
const currentIndexPatternId = availableIndexPatternIds[0]!;
|
||||
|
||||
if (currentIndexPatternId) {
|
||||
setLastUsedIndexPatternId(storage, currentIndexPatternId);
|
||||
}
|
||||
|
||||
if (!requiredPatterns.includes(currentIndexPatternId)) {
|
||||
requiredPatterns.push(currentIndexPatternId);
|
||||
}
|
||||
const indexPatterns = await loadIndexPatterns({
|
||||
indexPatternsService,
|
||||
cache: {},
|
||||
|
@ -263,13 +292,17 @@ export async function changeIndexPattern({
|
|||
storage: IStorageWrapper;
|
||||
indexPatternsService: IndexPatternsService;
|
||||
}) {
|
||||
try {
|
||||
const indexPatterns = await loadIndexPatterns({
|
||||
indexPatternsService,
|
||||
cache: state.indexPatterns,
|
||||
patterns: [id],
|
||||
});
|
||||
const indexPatterns = await loadIndexPatterns({
|
||||
indexPatternsService,
|
||||
cache: state.indexPatterns,
|
||||
patterns: [id],
|
||||
});
|
||||
|
||||
if (indexPatterns[id] == null) {
|
||||
return onError(Error('Missing indexpatterns'));
|
||||
}
|
||||
|
||||
try {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
layers: isSingleEmptyLayer(state.layers)
|
||||
|
@ -306,13 +339,16 @@ export async function changeLayerIndexPattern({
|
|||
storage: IStorageWrapper;
|
||||
indexPatternsService: IndexPatternsService;
|
||||
}) {
|
||||
try {
|
||||
const indexPatterns = await loadIndexPatterns({
|
||||
indexPatternsService,
|
||||
cache: state.indexPatterns,
|
||||
patterns: [indexPatternId],
|
||||
});
|
||||
const indexPatterns = await loadIndexPatterns({
|
||||
indexPatternsService,
|
||||
cache: state.indexPatterns,
|
||||
patterns: [indexPatternId],
|
||||
});
|
||||
if (indexPatterns[indexPatternId] == null) {
|
||||
return onError(Error('Missing indexpatterns'));
|
||||
}
|
||||
|
||||
try {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
layers: {
|
||||
|
|
|
@ -30,7 +30,7 @@ function getExpressionForLayer(
|
|||
uiSettings: IUiSettingsClient
|
||||
): ExpressionAstExpression | null {
|
||||
const { columns, columnOrder } = layer;
|
||||
if (columnOrder.length === 0) {
|
||||
if (columnOrder.length === 0 || !indexPattern) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -183,6 +183,13 @@ export class LensPlugin {
|
|||
return ContextProvider;
|
||||
};
|
||||
|
||||
const ensureDefaultIndexPattern = async () => {
|
||||
const [, deps] = await core.getStartServices();
|
||||
// make sure a default index pattern exists
|
||||
// if not, the page will be redirected to management and visualize won't be rendered
|
||||
await deps.data.indexPatterns.ensureDefaultIndexPattern();
|
||||
};
|
||||
|
||||
core.application.register({
|
||||
id: APP_ID,
|
||||
title: NOT_INTERNATIONALIZED_PRODUCT_NAME,
|
||||
|
@ -190,6 +197,7 @@ export class LensPlugin {
|
|||
mount: async (params: AppMountParameters) => {
|
||||
const { mountApp, stopReportManager } = await import('./async_services');
|
||||
this.stopReportManager = stopReportManager;
|
||||
await ensureDefaultIndexPattern();
|
||||
return mountApp(core, params, {
|
||||
createEditorFrame: this.createEditorFrame!,
|
||||
attributeService: this.attributeService!,
|
||||
|
|
|
@ -224,6 +224,10 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
* uniqueLabels of dimensions exposed for aria-labels of dragged dimensions
|
||||
*/
|
||||
uniqueLabels: (state: T) => Record<string, string>;
|
||||
/**
|
||||
* Check the internal state integrity and returns a list of missing references
|
||||
*/
|
||||
checkIntegrity: (state: T) => string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,3 +23,7 @@
|
|||
justify-content: center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.lnsSelectableErrorMessage {
|
||||
user-select: text;
|
||||
}
|
||||
|
|
|
@ -11804,7 +11804,6 @@
|
|||
"xpack.lens.app.docLoadingError": "保存されたドキュメントの保存中にエラーが発生",
|
||||
"xpack.lens.app.downloadButtonAriaLabel": "データを CSV ファイルとしてダウンロード",
|
||||
"xpack.lens.app.downloadCSV": "CSV をダウンロード",
|
||||
"xpack.lens.app.indexPatternLoadingError": "インデックスパターンの読み込み中にエラーが発生",
|
||||
"xpack.lens.app.save": "保存",
|
||||
"xpack.lens.app.saveAndReturn": "保存して戻る",
|
||||
"xpack.lens.app.saveAndReturnButtonAriaLabel": "現在のLensビジュアライゼーションを保存し、前回使用していたアプリに戻る",
|
||||
|
|
|
@ -11960,7 +11960,6 @@
|
|||
"xpack.lens.app.docLoadingError": "加载已保存文档时出错",
|
||||
"xpack.lens.app.downloadButtonAriaLabel": "将数据下载为 CSV 文件",
|
||||
"xpack.lens.app.downloadCSV": "下载为 CSV",
|
||||
"xpack.lens.app.indexPatternLoadingError": "加载索引模式时出错",
|
||||
"xpack.lens.app.save": "保存",
|
||||
"xpack.lens.app.saveAndReturn": "保存并返回",
|
||||
"xpack.lens.app.saveAndReturnButtonAriaLabel": "保存当前 Lens 可视化并返回到上一应用",
|
||||
|
|
Loading…
Reference in a new issue