[Logs+] Add All entry for DatasetSelector (#160971)

## 📓 Summary

Closes #160146 

This PR adds the entry to allow users to select a dataset that creates a
`logs-*-*` dataview.

Although the presentational and UX changes are minimal for the user,
this work lays the foundation for [restoring the dataset selection from
the URL](https://github.com/elastic/kibana/issues/160425) and for the
[dataset multi-selection
feature](https://github.com/elastic/observability-dev/issues/2744).

The core changes for this implementation consist of:
- Update DatasetSelector state machine to manage two parallel states: a
`popover` one to manage the navigation on the selector and a `selection`
state that handles the selection modes (currently only `all` and
`single`, but ready to also implement `multi`)
<img width="1522" alt="state-machine"
src="c240e5d5-6a38-4d08-b893-117132477896">

- DatasetSelector is now a controlled component regarding the selection
value: it will react to the injected `datasetSelection` property, and
notify the parent component of any change with the `onSelectionChange`
event handler.
This will allow us to always reinitialize the DatasetSelector state
machine from the URL state and fall back to the chosen selection in case
there is no one to restore.


4887b1d4-63ba-476b-a74f-5b4a9504f939

## Architectural choices

- The state machine will handle the two states in parallel such that we
can switch to available selection modes depending on the interactions
without clashing with the independent selector navigation.
- The `DatasetSelection` data structure is now what represents the state
selection in different modes.
The three available modes (counting also `multi`, but not implemented
yet), differs in mostly any aspect:
  - Internal data structure shape
  - DataViewSpecs composition
  - Payload to encode into a URL state
- Extraction of the presentational values (title, icons, and with
`multi` also the datasets which are currently selected)

With all these differences but the same final purposes of creating an
ad-hoc DataView and storing encoding a URL state to store, applying the
concepts for the Strategy pattern seemed the most sensed option to me.
This allows us to scale and add new selection modes (`multi`) respecting
the existing strategies contract and encapsulating all the concerned
transformations and parsing.

Encoding and decoding of the Dataview id that will be used for restoring
from the URL are already created and will be handy for the upcoming
tasks.

---------

Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Antonio Ghiani 2023-07-04 09:10:05 +02:00 committed by GitHub
parent be4a4d74d3
commit b9e2fdb6a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 635 additions and 151 deletions

View file

@ -5,19 +5,26 @@
* 2.0.
*/
import { IconType } from '@elastic/eui';
import { DataViewSpec } from '@kbn/data-views-plugin/common';
import { IndexPattern } from '@kbn/io-ts-utils';
import { DatasetId, DatasetType, IntegrationType } from '../types';
type IntegrationBase = Pick<IntegrationType, 'name' | 'version'>;
interface DatasetDeps extends DatasetType {
iconType?: IconType;
}
export class Dataset {
id: DatasetId;
iconType?: IconType;
name: DatasetType['name'];
title: DatasetType['title'];
title: string;
parentIntegration?: IntegrationBase;
private constructor(dataset: DatasetType, parentIntegration?: IntegrationType) {
private constructor(dataset: DatasetDeps, parentIntegration?: IntegrationBase) {
this.id = `dataset-${dataset.name}` as DatasetId;
this.iconType = dataset.iconType;
this.name = dataset.name;
this.title = dataset.title ?? dataset.name;
this.parentIntegration = parentIntegration && {
@ -26,16 +33,37 @@ export class Dataset {
};
}
getFullTitle(): string {
return this.parentIntegration?.name
? `[${this.parentIntegration.name}] ${this.title}`
: this.title;
}
toDataviewSpec(): DataViewSpec {
// Invert the property because the API returns the index pattern as `name` and a readable name as `title`
return {
id: this.id,
name: this.title,
title: this.name,
name: this.getFullTitle(),
title: this.name as string,
};
}
public static create(dataset: DatasetType, parentIntegration?: IntegrationType) {
toPlain() {
return {
name: this.name,
title: this.title,
};
}
public static create(dataset: DatasetDeps, parentIntegration?: IntegrationBase) {
return new Dataset(dataset, parentIntegration);
}
public static createAllLogsDataset() {
return new Dataset({
name: 'logs-*-*' as IndexPattern,
title: 'All log datasets',
iconType: 'editorChecklist',
});
}
}

View file

@ -13,11 +13,12 @@ import type { Meta, Story } from '@storybook/react';
import { IndexPattern } from '@kbn/io-ts-utils';
import { Dataset, Integration } from '../../../common/datasets';
import { DatasetSelector } from './dataset_selector';
import { DatasetSelectorProps, DatasetsSelectorSearchParams } from './types';
import {
DatasetSelectionHandler,
DatasetSelectorProps,
DatasetsSelectorSearchParams,
} from './types';
AllDatasetSelection,
DatasetSelection,
DatasetSelectionChange,
} from '../../utils/dataset_selection';
const meta: Meta<typeof DatasetSelector> = {
component: DatasetSelector,
@ -37,7 +38,9 @@ const meta: Meta<typeof DatasetSelector> = {
export default meta;
const DatasetSelectorTemplate: Story<DatasetSelectorProps> = (args) => {
const [selected, setSelected] = useState<Dataset>(() => mockIntegrations[0].datasets[0]);
const [datasetSelection, setDatasetSelection] = useState<DatasetSelection>(() =>
AllDatasetSelection.create()
);
const [search, setSearch] = useState<DatasetsSelectorSearchParams>({
sortOrder: 'asc',
@ -51,8 +54,8 @@ const DatasetSelectorTemplate: Story<DatasetSelectorProps> = (args) => {
}
};
const onDatasetSelected: DatasetSelectionHandler = (dataset) => {
setSelected(dataset);
const onSelectionChange: DatasetSelectionChange = (newSelection) => {
setDatasetSelection(newSelection);
};
const filteredIntegrations = integrations.filter((integration) =>
@ -72,14 +75,14 @@ const DatasetSelectorTemplate: Story<DatasetSelectorProps> = (args) => {
<DatasetSelector
{...args}
datasets={sortedDatasets}
initialSelected={selected}
datasetSelection={datasetSelection}
integrations={sortedIntegrations}
onIntegrationsLoadMore={onIntegrationsLoadMore}
onIntegrationsSearch={setSearch}
onIntegrationsSort={setSearch}
onIntegrationsStreamsSearch={setSearch}
onIntegrationsStreamsSort={setSearch}
onDatasetSelected={onDatasetSelected}
onSelectionChange={onSelectionChange}
onUnmanagedStreamsSearch={setSearch}
onUnmanagedStreamsSort={setSearch}
/>

View file

@ -22,7 +22,12 @@ import { DatasetsPopover } from './sub_components/datasets_popover';
import { DatasetSkeleton } from './sub_components/datasets_skeleton';
import { SearchControls } from './sub_components/search_controls';
import { DatasetSelectorProps } from './types';
import { buildIntegrationsTree } from './utils';
import {
buildIntegrationsTree,
createAllLogDatasetsItem,
createUnmanagedDatasetsItem,
createIntegrationStatusItem,
} from './utils';
/**
* Lazy load hidden components
@ -30,12 +35,11 @@ import { buildIntegrationsTree } from './utils';
const DatasetsList = dynamic(() => import('./sub_components/datasets_list'), {
fallback: <DatasetSkeleton />,
});
const IntegrationsListStatus = dynamic(() => import('./sub_components/integrations_list_status'));
export function DatasetSelector({
datasets,
datasetsError,
initialSelected,
datasetSelection,
integrations,
integrationsError,
isLoadingIntegrations,
@ -46,7 +50,7 @@ export function DatasetSelector({
onIntegrationsSort,
onIntegrationsStreamsSearch,
onIntegrationsStreamsSort,
onDatasetSelected,
onSelectionChange,
onStreamsEntryClick,
onUnmanagedStreamsReload,
onUnmanagedStreamsSearch,
@ -56,16 +60,16 @@ export function DatasetSelector({
isOpen,
panelId,
search,
selected,
closePopover,
changePanel,
scrollToIntegrationsBottom,
searchByName,
selectAllLogDataset,
selectDataset,
sortByOrder,
togglePopover,
} = useDatasetSelector({
initialContext: { selected: initialSelected },
initialContext: { selection: datasetSelection },
onIntegrationsLoadMore,
onIntegrationsReload,
onIntegrationsSearch,
@ -75,32 +79,26 @@ export function DatasetSelector({
onUnmanagedStreamsSearch,
onUnmanagedStreamsSort,
onUnmanagedStreamsReload,
onDatasetSelected,
onSelectionChange,
});
const [setSpyRef] = useIntersectionRef({ onIntersecting: scrollToIntegrationsBottom });
const { items: integrationItems, panels: integrationPanels } = useMemo(() => {
const datasetsItem = {
name: uncategorizedLabel,
onClick: onStreamsEntryClick,
panel: UNMANAGED_STREAMS_PANEL_ID,
};
const createIntegrationStatusItem = () => ({
disabled: true,
name: (
<IntegrationsListStatus
error={integrationsError}
integrations={integrations}
onRetry={onIntegrationsReload}
/>
),
});
const allLogDatasetsItem = createAllLogDatasetsItem({ onClick: selectAllLogDataset });
const unmanagedDatasetsItem = createUnmanagedDatasetsItem({ onClick: onStreamsEntryClick });
if (!integrations || integrations.length === 0) {
return {
items: [datasetsItem, createIntegrationStatusItem()],
items: [
allLogDatasetsItem,
unmanagedDatasetsItem,
createIntegrationStatusItem({
error: integrationsError,
integrations,
onRetry: onIntegrationsReload,
}),
],
panels: [],
};
}
@ -112,12 +110,13 @@ export function DatasetSelector({
});
return {
items: [datasetsItem, ...items],
items: [allLogDatasetsItem, unmanagedDatasetsItem, ...items],
panels,
};
}, [
integrations,
integrationsError,
selectAllLogDataset,
selectDataset,
onIntegrationsReload,
onStreamsEntryClick,
@ -150,7 +149,7 @@ export function DatasetSelector({
return (
<DatasetsPopover
selected={selected}
selection={datasetSelection.selection}
isOpen={isOpen}
closePopover={closePopover}
onClick={togglePopover}
@ -169,6 +168,7 @@ export function DatasetSelector({
onPanelChange={changePanel}
className="eui-yScroll"
css={contextMenuStyles}
size="s"
/>
</DatasetsPopover>
);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { AllDatasetSelection } from '../../../utils/dataset_selection';
import { HashedCache } from '../../../../common/hashed_cache';
import { INTEGRATION_PANEL_ID } from '../constants';
import { DatasetsSelectorSearchParams } from '../types';
@ -16,6 +17,7 @@ export const defaultSearch: DatasetsSelectorSearchParams = {
};
export const DEFAULT_CONTEXT: DefaultDatasetsSelectorContext = {
selection: AllDatasetSelection.create(),
searchCache: new HashedCache(),
panelId: INTEGRATION_PANEL_ID,
search: defaultSearch,

View file

@ -6,6 +6,7 @@
*/
import { actions, assign, createMachine, raise } from 'xstate';
import { AllDatasetSelection, SingleDatasetSelection } from '../../../utils/dataset_selection';
import { UNMANAGED_STREAMS_PANEL_ID } from '../constants';
import { defaultSearch, DEFAULT_CONTEXT } from './defaults';
import {
@ -19,83 +20,107 @@ import {
export const createPureDatasetsSelectorStateMachine = (
initialContext: Partial<DefaultDatasetsSelectorContext> = DEFAULT_CONTEXT
) =>
/** @xstate-layout N4IgpgJg5mDOIC5QBECGAXVsztgZTABswBjdAewCcA6Ew87CAYgBUB5AcQ4BkBRAbQAMAXUSgADgwCW6KeQB2YkAA9EAVgBMAGhABPRAA4AjNTWDzggJxqjAFgODbANicBfVzrSZsuAsTJU1OTiYPJMAMLcbHgCIkqSsDJyikgqiEZGLtTO9hrWBgZqThpqOvoItraC1EYA7E6CdgYAzIJFghrN7p4YWDj4RKQUNMGhrJw8saKpCUkKSqoIGQ3ULrWWjU6WzbW2dmXqzbbZarUaxmZqzoJuHiBefb6DASMh8tSEUrCy8lAAkvJ0GAoJQMMlYBEABIAQQAchxeAB9AAKcN43CE0wk0lk81Si1szRMLTU2xsTk0RksBgOCEstQM1B2Ng6jlsGnOtW6916PgG-mGQTeHy+P3+gOBoNx8gh4Rh8KRqNh6P4RixIFm0oWiEJxOapP1mUp1NpRUs1BuGlqzX1DnaGm5Dz5fiGgVG70+3ykvwBQJBYIUELw4QASmxuNxEexEX9YSxeBwQ9CWH82LC8IiAEJsFjsACymPiOOS2oQBlqFosVerzVp9Q0NVqVJtnWpxiMjt5-RdLyFoRFXp9Ev90qDvGhIblWYAmojYdC81Mi4ktfj0ldVpY7OZ2fTbKcaXpEE5moz6mpSQyjESbAZO95u89Be6B2LfZKAzKmHg2CGWDPEV-ZBeBDQsZmLPFQEWTIGxaDRBCJKwMlOIxTSMNQmVJDRilPJx2Ww+9Hn5V1Xn7T032HKVkjwdBKDAVAAFtZXlBEUTRDE4nAlcSzXBBigwqk1COWp1gMSx6UPcpb1MYomy2NoXEI50nzdYVyO9cU-SohQaLoxixz4cJ-2QZNoRiFgwOxbjILSJZr0ZJx1kcm1TyKEpaQpBtLXqLZWmaJwOzuJ1HwFVSyNFDT3xHajaPopjv3HSdIQA+dF0sjUIJSKDDCqahsKKIwEJbIw8lpZpLGOdCT1k7YGXLLogq7J5QtIj0IqHLTP10uKg1-f9M1nICQPSzUeOyst7ErcrCo6ToOUsWkrWqWozA6a1HLaCqGp6B9mpIvs2sHKAAFV5AY1B5FQGAIG6-SoThVilRVTirLmLLbP4mprGE0TxIZWlLCtahyzMQp-O2c5bh2oie2fNT2pOs6LquyBbvimJDOM0zzJGzLSwyU9Vic60bUKfiAcqbINCpdkXBaOxbCUkL9pfdTflO87LuutGxwnKcBrnBcly4t7SwMXLGgK6m5v82paQcZpsicMTsLsalKi5RrduI3tWYRjnke52K7p-P8AKG0CXoy6z3sWcWKwMeCrWw2oOjpjyHAtWSyfQsw8ncO55HICA4CUYK9peZdRd4gBaRlxITxPE7OWkY4wpPHEKQSjiZiPBToBhICj1dxuvQkvqEuoxNw6wPMcGSrT9i9rihnltdhsL3tGmzFnKytqwH2sjyWFo8tWq1Zc2wlc51uH+wAC1FYuxtswqnAb+w6lPHYjEd2l92OfVtgCtp1gvQLoeUlqDtfSLKM-eARZL1ebg34xrRaJtxflk9VhKRwSpbAZCJGeHdWq3w6h+aUPNl490MIICslhlZnAaMsfUpoOQNy2I7eCt4L5txhipcBbNEacxRjdY2TFYG23gYg5B2FGiZHQcPbYxIVo3DpnsAwFIA6uCAA */
/** @xstate-layout N4IgpgJg5mDOIC5QBECGAXVsztgZTABswBjdAewCcA6AB3PoDcwaTDzsIBiAFQHkA4gIAyAUQDaABgC6iUPVgBLdIvIA7OSAAeiALQBmAKzUALAEZDAdgBslk4ckAmEwA5HhkwBoQAT0T6LakdHFzMLMLN9dzsAXxjvNExsXAJiMio6BnJmGgYwNS4AYWE+PAkZTQVlVQ0kbUQzSQBOY0szd31zE0lrJutrbz8EVxdTM0solya3YP64hIwsHHwiUgoaeiYWajyC-iExKVk6qpV1TR0ERr7qa0cwx0lJE0tLF0l9QcQTbtveyXa1gB+j6LnmIESSxSq3SGyyOR2tHy1EIilgKjUUAAkmp0GAoJQMDVYEUABIAQQAcgJRAB9AAKVNEwiOlQ41XOdUu+iit0MhjMtgsnQFjgGvkQTQBQV6YXMhncj0c4MhyRWaXWmS2uSRahRaIx2Nx+MJZzUJMKFOpdMZlOZ4jMx3k7LNF0QhiBpn6Hh+7kMTU+EoQ70s1EkHmBjiM+ksTWV8QhizVqTWGU22W2u316MUmJxeIJRPUJLwhQASnxhMJafxaVjKTxRAIy+SeFi+JS8LSAEJ8Hj8ACyrJOLpqboQujMJn01Bad2e7m6Fi8QcsYuoCpMTXGTl9thVSeWKdhWozOuRqJzeeNhbNJdE5LLlp7AE1aZTyQPyk6QKcx1y9DMJomlnBxHDjacWhcCZLC+YN3GoSwekkN4106awpgPJIjxhTV0wRLNL0NfMTSLc0uDwPgyx4V9aSo5BRDLYdnSUV0AInMJjHuCYxSaJDgiQww4MaZ5Z3MZxXHcFwFUsLCoXVVM4W1RELwNXMjQLU1iQo5lREKGjySrWkSgELtkFbckyh4Zjf1HTlQEuRobDDGwjG3cwzCmcUhhcEEgj6R4XGsF5DDk5NcLTeFM11bNiJvLT1DwdBKDAVAAFsLStGkGSZFkKhHVj-wc-wFWoRoRRE-QAUMFw4N6UNrE8qwWgmHkarCnCNUi5TCLU69NLIpKUvS+9H2fbs3w-L8bL-ez6gQBwTFMKMozXUIPSqoSgy3JbBOA-R-iicZQoTVVOsU08CJioj1JI28aiG1KMooqiaImuiywYpj8pYjlamKhBYyW7pmmeMIWhMZw4J2xDkMkKYtzCeGOuhLqlLPFS9Ru-rSLNR6Rp0sR9NpcyeEs0RrJ+2zCrmxz4eMH4mheYCXmeWqgwwyRqCCtdGtsD0ejBU7D1Ri78Oi1SrygABVNQ0tQNRUBgCB8eey0qWy217Sp2b-vmzpHGoEE7n5qMQ1goNGnMI2mYO6YpJklGFJPcXzyxvqZblhWlcgVXRqfUlaKm782RpvXLhko2nPpxwkMFQMhk44wHHCaN-T4kwnePPCord2L1Nl+XFeVv2Xuo2j6MYma7PDxA3FGPjt1trcmYmYSLGT6rIkMNyM6ziL0auyXDUL72S+Sp77yJmjSfJymf118cnOsMNIZMDC1wcBUtsT54QKlF4tysRH1-7tHqGwDUagv9TiEJvSDKMkyzIsqzq7D8cMKCbpIOcRqgpQnBAUG43BAm6Hbe4LghYLGwqLE8l81jXyUJiO+ZRp4k1fhTd+f1xy6D-rDDwrVRS2G8noA6MpBRAjXNuAB8YYHyWzhkBBZBr6oEIIQe+xNZ5vx1jXcc25uYBACL5VwUppgJz0K8bm1gapRgBFOeucQExqHIBAOAmgzpwPWKHHB7EDDTlAk4CCIIap2Dgng0qQVZEfGnAEV40DEywOdjnbUOi2IAzwS0Qx4FnAmOgiuIYAlEIhGXLxHunQz5i1ztQNgHBIBuKKvNSIRhubTFavYSIHw4IxlDN0f+bwarhkaNYSJLtom7ASbTPQzhQwtCMb4qCMFoY9GkeBKc0xXABBKcLJxjDB4Sz1AACwNJU2uE4lpPHlL5HofRmqOGhkzI2dxtwHQcHYbcpSXEY16lLO6CVzSjKXlzIw9gpRVTuCCdmPlgE2G3FOCwionCbO6ts66Hs9mDQniNQ57F-SG3kSEIK3EXD2DqlORCnk2gRBsSEZ5-S87Y09kXH2KsvkZR+QDGqS0oFM39HYFOUxhJPC5j-dZx93LdPoeFc+zD3HU10R48Cu1wwvDaoCGwATAIyKWbIwUrwQawp6QwgeF9cJINvmADF81dC2EQrGEFTgeRFLaHBN4phtz+k2h6DCvk4WiqvuoagbDCBSu5NymMwV3BOCsDGTlwYubAQsAGcM2qoH6CUTEIAA */
createMachine<DatasetsSelectorContext, DatasetsSelectorEvent, DatasetsSelectorTypestate>(
{
context: { ...DEFAULT_CONTEXT, ...initialContext },
preserveActionOrder: true,
predictableActionArguments: true,
id: 'DatasetsSelector',
initial: 'closed',
type: 'parallel',
states: {
closed: {
id: 'closed',
on: {
TOGGLE: 'open.hist',
popover: {
initial: 'closed',
states: {
closed: {
id: 'closed',
on: {
TOGGLE: 'open.hist',
},
},
open: {
initial: 'listingIntegrations',
on: {
CLOSE: 'closed',
TOGGLE: 'closed',
},
states: {
hist: {
type: 'history',
},
listingIntegrations: {
entry: ['storePanelId', 'retrieveSearchFromCache', 'maybeRestoreSearchResult'],
on: {
CHANGE_PANEL: [
{
cond: 'isUnmanagedStreamsId',
target: 'listingUnmanagedStreams',
},
{
target: 'listingIntegrationStreams',
},
],
SCROLL_TO_INTEGRATIONS_BOTTOM: {
actions: 'loadMoreIntegrations',
},
SEARCH_BY_NAME: {
actions: ['storeSearch', 'searchIntegrations'],
},
SORT_BY_ORDER: {
actions: ['storeSearch', 'sortIntegrations'],
},
SELECT_ALL_LOGS_DATASET: '#closed',
},
},
listingIntegrationStreams: {
entry: ['storePanelId', 'retrieveSearchFromCache', 'maybeRestoreSearchResult'],
on: {
CHANGE_PANEL: 'listingIntegrations',
SEARCH_BY_NAME: {
actions: ['storeSearch', 'searchIntegrationsStreams'],
},
SORT_BY_ORDER: {
actions: ['storeSearch', 'sortIntegrationsStreams'],
},
SELECT_DATASET: '#closed',
},
},
listingUnmanagedStreams: {
entry: ['storePanelId', 'retrieveSearchFromCache', 'maybeRestoreSearchResult'],
on: {
CHANGE_PANEL: 'listingIntegrations',
SEARCH_BY_NAME: {
actions: ['storeSearch', 'searchUnmanagedStreams'],
},
SORT_BY_ORDER: {
actions: ['storeSearch', 'sortUnmanagedStreams'],
},
SELECT_DATASET: '#closed',
},
},
},
},
},
},
open: {
initial: 'listingIntegrations',
on: {
CLOSE: 'closed',
TOGGLE: 'closed',
},
selection: {
initial: 'single',
states: {
hist: {
type: 'history',
},
listingIntegrations: {
entry: ['storePanelId', 'retrieveSearchFromCache', 'maybeRestoreSearchResult'],
single: {
on: {
CHANGE_PANEL: [
{
cond: 'isUnmanagedStreamsId',
target: 'listingUnmanagedStreams',
},
{
target: 'listingIntegrationStreams',
},
],
SCROLL_TO_INTEGRATIONS_BOTTOM: {
actions: 'loadMoreIntegrations',
SELECT_ALL_LOGS_DATASET: {
actions: ['storeAllSelection', 'notifySelectionChanged'],
target: 'all',
},
SEARCH_BY_NAME: {
actions: ['storeSearch', 'searchIntegrations'],
},
SORT_BY_ORDER: {
actions: ['storeSearch', 'sortIntegrations'],
SELECT_DATASET: {
actions: ['storeSingleSelection', 'notifySelectionChanged'],
},
},
},
listingIntegrationStreams: {
entry: ['storePanelId', 'retrieveSearchFromCache', 'maybeRestoreSearchResult'],
all: {
on: {
CHANGE_PANEL: 'listingIntegrations',
SELECT_DATASET: {
actions: ['storeSelected', 'selectStream'],
target: '#closed',
},
SEARCH_BY_NAME: {
actions: ['storeSearch', 'searchIntegrationsStreams'],
},
SORT_BY_ORDER: {
actions: ['storeSearch', 'sortIntegrationsStreams'],
},
},
},
listingUnmanagedStreams: {
entry: ['storePanelId', 'retrieveSearchFromCache', 'maybeRestoreSearchResult'],
on: {
CHANGE_PANEL: 'listingIntegrations',
SELECT_DATASET: {
actions: ['storeSelected', 'selectStream'],
target: '#closed',
},
SEARCH_BY_NAME: {
actions: ['storeSearch', 'searchUnmanagedStreams'],
},
SORT_BY_ORDER: {
actions: ['storeSearch', 'sortUnmanagedStreams'],
actions: ['storeSingleSelection', 'notifySelectionChanged'],
target: 'single',
},
},
},
@ -118,8 +143,11 @@ export const createPureDatasetsSelectorStateMachine = (
}
return {};
}),
storeSelected: assign((_context, event) =>
'dataset' in event ? { selected: event.dataset } : {}
storeAllSelection: assign((_context) => ({
selection: AllDatasetSelection.create(),
})),
storeSingleSelection: assign((_context, event) =>
'dataset' in event ? { selection: SingleDatasetSelection.create(event.dataset) } : {}
),
retrieveSearchFromCache: assign((context, event) =>
'panelId' in event
@ -150,15 +178,13 @@ export const createDatasetsSelectorStateMachine = ({
onIntegrationsStreamsSort,
onUnmanagedStreamsSearch,
onUnmanagedStreamsSort,
onDatasetSelected,
onSelectionChange,
onUnmanagedStreamsReload,
}: DatasetsSelectorStateMachineDependencies) =>
createPureDatasetsSelectorStateMachine(initialContext).withConfig({
actions: {
selectStream: (_context, event) => {
if ('dataset' in event) {
return onDatasetSelected(event.dataset);
}
notifySelectionChanged: (context) => {
return onSelectionChange(context.selection);
},
loadMoreIntegrations: onIntegrationsLoadMore,
relaodIntegrations: onIntegrationsReload,

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DatasetSelection, DatasetSelectionChange } from '../../../utils/dataset_selection';
import { Dataset } from '../../../../common/datasets/models/dataset';
import { ReloadDatasets, SearchDatasets } from '../../../hooks/use_datasets';
import {
@ -12,10 +13,10 @@ import {
SearchIntegrations,
} from '../../../hooks/use_integrations';
import type { IHashedCache } from '../../../../common/hashed_cache';
import { DatasetSelectionHandler, DatasetsSelectorSearchParams, PanelId } from '../types';
import { DatasetsSelectorSearchParams, PanelId } from '../types';
export interface DefaultDatasetsSelectorContext {
selected?: Dataset;
selection: DatasetSelection;
panelId: PanelId;
searchCache: IHashedCache<PanelId, DatasetsSelectorSearchParams>;
search: DatasetsSelectorSearchParams;
@ -23,27 +24,43 @@ export interface DefaultDatasetsSelectorContext {
export type DatasetsSelectorTypestate =
| {
value: 'closed';
value: 'popover';
context: DefaultDatasetsSelectorContext;
}
| {
value: 'open';
value: 'popover.closed';
context: DefaultDatasetsSelectorContext;
}
| {
value: { open: 'hist' };
value: 'popover.open';
context: DefaultDatasetsSelectorContext;
}
| {
value: { open: 'listingIntegrations' };
value: 'popover.open.hist';
context: DefaultDatasetsSelectorContext;
}
| {
value: { open: 'listingIntegrationStreams' };
value: 'popover.open.listingIntegrations';
context: DefaultDatasetsSelectorContext;
}
| {
value: { open: 'listingUnmanagedStreams' };
value: 'popover.open.listingIntegrationStreams';
context: DefaultDatasetsSelectorContext;
}
| {
value: 'popover.open.listingUnmanagedStreams';
context: DefaultDatasetsSelectorContext;
}
| {
value: 'selection';
context: DefaultDatasetsSelectorContext;
}
| {
value: 'selection.single';
context: DefaultDatasetsSelectorContext;
}
| {
value: 'selection.all';
context: DefaultDatasetsSelectorContext;
};
@ -64,6 +81,9 @@ export type DatasetsSelectorEvent =
type: 'SELECT_DATASET';
dataset: Dataset;
}
| {
type: 'SELECT_ALL_LOGS_DATASET';
}
| {
type: 'SCROLL_TO_INTEGRATIONS_BOTTOM';
}
@ -84,7 +104,7 @@ export interface DatasetsSelectorStateMachineDependencies {
onIntegrationsSort: SearchIntegrations;
onIntegrationsStreamsSearch: SearchIntegrations;
onIntegrationsStreamsSort: SearchIntegrations;
onDatasetSelected: DatasetSelectionHandler;
onSelectionChange: DatasetSelectionChange;
onUnmanagedStreamsReload: ReloadDatasets;
onUnmanagedStreamsSearch: SearchDatasets;
onUnmanagedStreamsSort: SearchDatasets;

View file

@ -24,9 +24,9 @@ export const useDatasetSelector = ({
onIntegrationsSort,
onIntegrationsStreamsSearch,
onIntegrationsStreamsSort,
onSelectionChange,
onUnmanagedStreamsSearch,
onUnmanagedStreamsSort,
onDatasetSelected,
onUnmanagedStreamsReload,
}: DatasetsSelectorStateMachineDependencies) => {
const datasetsSelectorStateService = useInterpret(() =>
@ -38,18 +38,19 @@ export const useDatasetSelector = ({
onIntegrationsSort,
onIntegrationsStreamsSearch,
onIntegrationsStreamsSort,
onSelectionChange,
onUnmanagedStreamsSearch,
onUnmanagedStreamsSort,
onDatasetSelected,
onUnmanagedStreamsReload,
})
);
const isOpen = useSelector(datasetsSelectorStateService, (state) => state.matches('open'));
const isOpen = useSelector(datasetsSelectorStateService, (state) =>
state.matches('popover.open')
);
const panelId = useSelector(datasetsSelectorStateService, (state) => state.context.panelId);
const search = useSelector(datasetsSelectorStateService, (state) => state.context.search);
const selected = useSelector(datasetsSelectorStateService, (state) => state.context.selected);
const changePanel = useCallback<ChangePanelHandler>(
(panelDetails) =>
@ -70,6 +71,11 @@ export const useDatasetSelector = ({
[datasetsSelectorStateService]
);
const selectAllLogDataset = useCallback(
() => datasetsSelectorStateService.send({ type: 'SELECT_ALL_LOGS_DATASET' }),
[datasetsSelectorStateService]
);
const selectDataset = useCallback<DatasetSelectionHandler>(
(dataset) => datasetsSelectorStateService.send({ type: 'SELECT_DATASET', dataset }),
[datasetsSelectorStateService]
@ -95,12 +101,12 @@ export const useDatasetSelector = ({
isOpen,
panelId,
search,
selected,
// Actions
closePopover,
changePanel,
scrollToIntegrationsBottom,
searchByName,
selectAllLogDataset,
selectDataset,
sortByOrder,
togglePopover,

View file

@ -9,6 +9,7 @@ import React from 'react';
import {
EuiButton,
EuiHorizontalRule,
EuiIcon,
EuiPanel,
EuiPopover,
EuiPopoverProps,
@ -17,7 +18,7 @@ import {
} from '@elastic/eui';
import styled from '@emotion/styled';
import { PackageIcon } from '@kbn/fleet-plugin/public';
import { Dataset } from '../../../../common/datasets/models/dataset';
import { DatasetSelection } from '../../../utils/dataset_selection';
import { DATA_VIEW_POPOVER_CONTENT_WIDTH, POPOVER_ID, selectDatasetLabel } from '../constants';
import { getPopoverButtonStyles } from '../utils';
@ -25,16 +26,17 @@ const panelStyle = { width: DATA_VIEW_POPOVER_CONTENT_WIDTH };
interface DatasetsPopoverProps extends Omit<EuiPopoverProps, 'button'> {
children: React.ReactNode;
onClick: () => void;
selected?: Dataset;
selection: DatasetSelection['selection'];
}
export const DatasetsPopover = ({
children,
onClick,
selected,
selection,
...props
}: DatasetsPopoverProps) => {
const { title, parentIntegration } = selected ?? {};
const { iconType, parentIntegration } = selection.dataset;
const title = selection.dataset.getFullTitle();
const isMobile = useIsWithinBreakpoints(['xs', 's']);
const buttonStyles = getPopoverButtonStyles({ fullWidth: isMobile });
@ -52,14 +54,16 @@ export const DatasetsPopover = ({
onClick={onClick}
fullWidth={isMobile}
>
{hasIntegration && (
{iconType ? (
<EuiIcon type={iconType} />
) : hasIntegration ? (
<PackageIcon
packageName={parentIntegration.name}
version={parentIntegration.version}
size="m"
tryApi
/>
)}
) : null}
<span className="eui-textTruncate">{title}</span>
</EuiButton>
}

View file

@ -17,7 +17,7 @@ import {
noIntegrationsLabel,
} from '../constants';
interface IntegrationsListStatusProps {
export interface IntegrationsListStatusProps {
integrations: Integration[] | null;
error: Error | null;
onRetry: ReloadIntegrations;

View file

@ -15,18 +15,19 @@ import {
SearchIntegrations,
} from '../../hooks/use_integrations';
import { INTEGRATION_PANEL_ID, UNMANAGED_STREAMS_PANEL_ID } from './constants';
import type { DatasetSelection, DatasetSelectionChange } from '../../utils/dataset_selection';
export interface DatasetSelectorProps {
/* The generic data stream list */
datasets: Dataset[] | null;
/* Any error occurred to show when the user preview the generic data streams */
datasetsError?: Error | null;
/* The integrations list, each integration includes its data streams */
initialSelected: Dataset;
/* The current selection instance */
datasetSelection: DatasetSelection;
/* The integrations list, each integration includes its data streams */
integrations: Integration[] | null;
/* Any error occurred to show when the user preview the integrations */
integrationsError?: Error | null;
integrationsError: Error | null;
/* Flags for loading/searching integrations or data streams*/
isLoadingIntegrations: boolean;
isLoadingStreams: boolean;
@ -45,8 +46,8 @@ export interface DatasetSelectorProps {
onUnmanagedStreamsReload: ReloadDatasets;
/* Triggered when the uncategorized streams entry is selected */
onStreamsEntryClick: LoadDatasets;
/* Triggered when a data stream entry is selected */
onDatasetSelected: DatasetSelectionHandler;
/* Triggered when the selection is updated */
onSelectionChange: DatasetSelectionChange;
}
export type PanelId =

View file

@ -6,11 +6,24 @@
*/
import React, { RefCallback } from 'react';
import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui';
import {
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
EuiIcon,
} from '@elastic/eui';
import { PackageIcon } from '@kbn/fleet-plugin/public';
import { Integration } from '../../../common/datasets';
import { DATA_VIEW_POPOVER_CONTENT_WIDTH } from './constants';
import { Dataset, Integration } from '../../../common/datasets';
import {
DATA_VIEW_POPOVER_CONTENT_WIDTH,
uncategorizedLabel,
UNMANAGED_STREAMS_PANEL_ID,
} from './constants';
import { DatasetSelectionHandler } from './types';
import { LoadDatasets } from '../../hooks/use_datasets';
import { dynamic } from '../../utils/dynamic';
import type { IntegrationsListStatusProps } from './sub_components/integrations_list_status';
const IntegrationsListStatus = dynamic(() => import('./sub_components/integrations_list_status'));
export const getPopoverButtonStyles = ({ fullWidth }: { fullWidth?: boolean }) => ({
maxWidth: fullWidth ? undefined : DATA_VIEW_POPOVER_CONTENT_WIDTH,
@ -67,3 +80,28 @@ export const buildIntegrationsTree = ({
{ items: [], panels: [] }
);
};
export const createAllLogDatasetsItem = ({ onClick }: { onClick(): void }) => {
const allLogDataset = Dataset.createAllLogsDataset();
return {
name: allLogDataset.title,
icon: allLogDataset.iconType && <EuiIcon type={allLogDataset.iconType} />,
onClick,
};
};
export const createUnmanagedDatasetsItem = ({ onClick }: { onClick: LoadDatasets }) => {
return {
name: uncategorizedLabel,
icon: <EuiIcon type="documents" />,
onClick,
panel: UNMANAGED_STREAMS_PANEL_ID,
};
};
export const createIntegrationStatusItem = (props: IntegrationsListStatusProps) => {
return {
disabled: true,
name: <IntegrationsListStatus {...props} />,
};
};

View file

@ -5,28 +5,37 @@
* 2.0.
*/
import React from 'react';
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState } from 'react';
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
import { IndexPattern } from '@kbn/io-ts-utils';
import { Dataset } from '../../common/datasets/models/dataset';
import { DatasetSelectionHandler, DatasetSelector } from '../components/dataset_selector';
import { DatasetSelector } from '../components/dataset_selector';
import { DatasetsProvider, useDatasetsContext } from '../hooks/use_datasets';
import { InternalStateProvider, useDataView } from '../hooks/use_data_view';
import { InternalStateProvider } from '../hooks/use_data_view';
import { IntegrationsProvider, useIntegrationsContext } from '../hooks/use_integrations';
import { IDatasetsClient } from '../services/datasets';
import {
AllDatasetSelection,
DatasetSelection,
DatasetSelectionChange,
} from '../utils/dataset_selection';
interface CustomDatasetSelectorProps {
stateContainer: DiscoverStateContainer;
}
export const CustomDatasetSelector = withProviders(({ stateContainer }) => {
// Container component, here goes all the state management and custom logic usage to keep the DatasetSelector presentational.
const dataView = useDataView();
/**
* TOREMOVE: This is a temporary workaround to control the datasetSelection value
* until we handle the restore/initialization of the dataview with https://github.com/elastic/kibana/issues/160425,
* where this value will be used to control the DatasetSelector selection with a top level state machine.
*/
const [datasetSelection, setDatasetSelection] = useState<DatasetSelection>(() =>
AllDatasetSelection.create()
);
const initialSelected: Dataset = Dataset.create({
name: dataView.getIndexPattern() as IndexPattern,
title: dataView.getName(),
});
// Restore All dataset selection on refresh until restore from url is not available
React.useEffect(() => handleStreamSelection(datasetSelection), []);
const {
error: integrationsError,
@ -54,15 +63,18 @@ export const CustomDatasetSelector = withProviders(({ stateContainer }) => {
* TODO: this action will be abstracted into a method of a class adapter in a follow-up PR
* since we'll need to handle more actions from the stateContainer
*/
const handleStreamSelection: DatasetSelectionHandler = (dataset) => {
return stateContainer.actions.onCreateDefaultAdHocDataView(dataset.toDataviewSpec());
const handleStreamSelection: DatasetSelectionChange = (nextDatasetSelection) => {
setDatasetSelection(nextDatasetSelection);
return stateContainer.actions.onCreateDefaultAdHocDataView(
nextDatasetSelection.toDataviewSpec()
);
};
return (
<DatasetSelector
datasets={datasets}
datasetSelection={datasetSelection}
datasetsError={datasetsError}
initialSelected={initialSelected}
integrations={integrations}
integrationsError={integrationsError}
isLoadingIntegrations={isLoadingIntegrations}
@ -73,7 +85,7 @@ export const CustomDatasetSelector = withProviders(({ stateContainer }) => {
onIntegrationsSort={sortIntegrations}
onIntegrationsStreamsSearch={searchIntegrationsStreams}
onIntegrationsStreamsSort={sortIntegrationsStreams}
onDatasetSelected={handleStreamSelection}
onSelectionChange={handleStreamSelection}
onStreamsEntryClick={loadDatasets}
onUnmanagedStreamsReload={reloadDatasets}
onUnmanagedStreamsSearch={searchDatasets}

View file

@ -0,0 +1,43 @@
/*
* 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 { Dataset } from '../../../common/datasets';
import { encodeDatasetSelection } from './encoding';
import { DatasetSelectionStrategy } from './types';
export class AllDatasetSelection implements DatasetSelectionStrategy {
selectionType: 'all';
selection: {
dataset: Dataset;
};
private constructor() {
this.selectionType = 'all';
this.selection = {
dataset: Dataset.createAllLogsDataset(),
};
}
toDataviewSpec() {
const { name, title } = this.selection.dataset.toDataviewSpec();
return {
id: this.toURLSelectionId(),
name,
title,
};
}
toURLSelectionId() {
return encodeDatasetSelection({
selectionType: this.selectionType,
});
}
public static create() {
return new AllDatasetSelection();
}
}

View file

@ -0,0 +1,100 @@
/*
* 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 { IndexPattern } from '@kbn/io-ts-utils';
import { encodeDatasetSelection, decodeDatasetSelectionId } from './encoding';
import { DatasetEncodingError } from './errors';
import { DatasetSelectionPlain } from './types';
describe('DatasetSelection', () => {
const allDatasetSelectionPlain: DatasetSelectionPlain = {
selectionType: 'all',
};
const encodedAllDatasetSelection = 'BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA';
const singleDatasetSelectionPlain: DatasetSelectionPlain = {
selectionType: 'single',
selection: {
name: 'azure',
version: '1.5.23',
dataset: {
name: 'logs-azure.activitylogs-*' as IndexPattern,
title: 'activitylogs',
},
},
};
const encodedSingleDatasetSelection =
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu0m8wrEEjTkAjBwCsHAEwBmcuvBQeKACqCADmSPJqUVUA==';
const invalidDatasetSelectionPlain = {
selectionType: 'single',
selection: {
dataset: {
// Missing mandatory `name` property
title: 'activitylogs',
},
},
};
const invalidCompressedId = 'random';
const invalidEncodedDatasetSelection = 'BQZwpgNmDGAuCWB7AdgFQJ4AcwC4T2QHMoBKIA==';
describe('#encodeDatasetSelection', () => {
test('should encode and compress a valid DatasetSelection plain object', () => {
// Encode AllDatasetSelection plain object
expect(encodeDatasetSelection(allDatasetSelectionPlain)).toEqual(encodedAllDatasetSelection);
// Encode SingleDatasetSelection plain object
expect(encodeDatasetSelection(singleDatasetSelectionPlain)).toEqual(
encodedSingleDatasetSelection
);
});
test('should throw a DatasetEncodingError if the input is an invalid DatasetSelection plain object', () => {
const encodingRunner = () =>
encodeDatasetSelection(invalidDatasetSelectionPlain as DatasetSelectionPlain);
expect(encodingRunner).toThrow(DatasetEncodingError);
expect(encodingRunner).toThrow(/^The current dataset selection is invalid/);
});
});
describe('#decodeDatasetSelectionId', () => {
test('should decode and decompress a valid encoded string', () => {
// Decode AllDatasetSelection plain object
expect(decodeDatasetSelectionId(encodedAllDatasetSelection)).toEqual(
allDatasetSelectionPlain
);
// Decode SingleDatasetSelection plain object
expect(decodeDatasetSelectionId(encodedSingleDatasetSelection)).toEqual(
singleDatasetSelectionPlain
);
});
test('should throw a DatasetEncodingError if the input is an invalid compressed id', () => {
expect(() => decodeDatasetSelectionId(invalidCompressedId)).toThrow(
new DatasetEncodingError('The stored id is not a valid compressed value.')
);
});
test('should throw a DatasetEncodingError if the decompressed value is an invalid DatasetSelection plain object', () => {
const decodingRunner = () => decodeDatasetSelectionId(invalidEncodedDatasetSelection);
expect(decodingRunner).toThrow(DatasetEncodingError);
expect(decodingRunner).toThrow(/^The current dataset selection is invalid/);
});
});
test('encoding and decoding should restore the original DatasetSelection plain object', () => {
// Encode/Decode AllDatasetSelection plain object
expect(decodeDatasetSelectionId(encodeDatasetSelection(allDatasetSelectionPlain))).toEqual(
allDatasetSelectionPlain
);
// Encode/Decode SingleDatasetSelection plain object
expect(decodeDatasetSelectionId(encodeDatasetSelection(singleDatasetSelectionPlain))).toEqual(
singleDatasetSelectionPlain
);
});
});

View file

@ -0,0 +1,40 @@
/*
* 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 { decode, encode, RisonValue } from '@kbn/rison';
import * as lz from 'lz-string';
import { decodeOrThrow } from '../../../common/runtime_types';
import { DatasetEncodingError } from './errors';
import { DatasetSelectionPlain, datasetSelectionPlainRT } from './types';
export const encodeDatasetSelection = (datasetSelectionPlain: DatasetSelectionPlain) => {
const safeDatasetSelection = decodeOrThrow(
datasetSelectionPlainRT,
(message: string) =>
new DatasetEncodingError(`The current dataset selection is invalid: ${message}"`)
)(datasetSelectionPlain);
return lz.compressToBase64(encode(safeDatasetSelection));
};
export const decodeDatasetSelectionId = (datasetSelectionId: string): DatasetSelectionPlain => {
const risonDatasetSelection: RisonValue = lz.decompressFromBase64(datasetSelectionId);
if (risonDatasetSelection === null || risonDatasetSelection === '') {
throw new DatasetEncodingError('The stored id is not a valid compressed value.');
}
const decodedDatasetSelection = decode(risonDatasetSelection);
const datasetSelection = decodeOrThrow(
datasetSelectionPlainRT,
(message: string) =>
new DatasetEncodingError(`The current dataset selection is invalid: ${message}"`)
)(decodedDatasetSelection);
return datasetSelection;
};

View file

@ -0,0 +1,14 @@
/*
* 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 class DatasetEncodingError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'DatasetEncodingError';
}
}

View file

@ -0,0 +1,19 @@
/*
* 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 { AllDatasetSelection } from './all_dataset_selection';
import { SingleDatasetSelection } from './single_dataset_selection';
import { DatasetSelectionPlain } from './types';
export const hydrateDatasetSelection = (datasetSelection: DatasetSelectionPlain) => {
if (datasetSelection.selectionType === 'all') {
return AllDatasetSelection.create();
}
if (datasetSelection.selectionType === 'single') {
return SingleDatasetSelection.fromSelection(datasetSelection.selection);
}
};

View file

@ -0,0 +1,18 @@
/*
* 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 { AllDatasetSelection } from './all_dataset_selection';
import { SingleDatasetSelection } from './single_dataset_selection';
export type DatasetSelection = AllDatasetSelection | SingleDatasetSelection;
export type DatasetSelectionChange = (datasetSelection: DatasetSelection) => void;
export * from './all_dataset_selection';
export * from './single_dataset_selection';
export * from './encoding';
export * from './hydrate_dataset_selection.ts';
export * from './types';

View file

@ -0,0 +1,62 @@
/*
* 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 { Dataset } from '../../../common/datasets';
import { encodeDatasetSelection } from './encoding';
import { DatasetSelectionStrategy, SingleDatasetSelectionPayload } from './types';
export class SingleDatasetSelection implements DatasetSelectionStrategy {
selectionType: 'single';
selection: {
name?: string;
version?: string;
dataset: Dataset;
};
private constructor(dataset: Dataset) {
this.selectionType = 'single';
this.selection = {
name: dataset.parentIntegration?.name,
version: dataset.parentIntegration?.version,
dataset,
};
}
toDataviewSpec() {
const { name, title } = this.selection.dataset.toDataviewSpec();
return {
id: this.toURLSelectionId(),
name,
title,
};
}
toURLSelectionId() {
return encodeDatasetSelection({
selectionType: this.selectionType,
selection: {
name: this.selection.name,
version: this.selection.version,
dataset: this.selection.dataset.toPlain(),
},
});
}
public static fromSelection(selection: SingleDatasetSelectionPayload) {
const { name, version, dataset } = selection;
// Attempt reconstructing the integration object
const integration = name && version ? { name, version } : undefined;
const datasetInstance = Dataset.create(dataset, integration);
return new SingleDatasetSelection(datasetInstance);
}
public static create(dataset: Dataset) {
return new SingleDatasetSelection(dataset);
}
}

View file

@ -0,0 +1,47 @@
/*
* 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 { DataViewSpec } from '@kbn/data-views-plugin/common';
import * as rt from 'io-ts';
import { datasetRT } from '../../../common/datasets';
export const allDatasetSelectionPlainRT = rt.type({
selectionType: rt.literal('all'),
});
const integrationNameRT = rt.partial({
name: rt.string,
});
const integrationVersionRT = rt.partial({
version: rt.string,
});
const singleDatasetSelectionPayloadRT = rt.intersection([
integrationNameRT,
integrationVersionRT,
rt.type({
dataset: datasetRT,
}),
]);
export const singleDatasetSelectionPlainRT = rt.type({
selectionType: rt.literal('single'),
selection: singleDatasetSelectionPayloadRT,
});
export const datasetSelectionPlainRT = rt.union([
allDatasetSelectionPlainRT,
singleDatasetSelectionPlainRT,
]);
export type SingleDatasetSelectionPayload = rt.TypeOf<typeof singleDatasetSelectionPayloadRT>;
export type DatasetSelectionPlain = rt.TypeOf<typeof datasetSelectionPlainRT>;
export interface DatasetSelectionStrategy {
toDataviewSpec(): DataViewSpec;
toURLSelectionId(): string;
}

View file

@ -13,6 +13,7 @@
"@kbn/kibana-utils-plugin",
"@kbn/io-ts-utils",
"@kbn/data-views-plugin",
"@kbn/rison",
],
"exclude": ["target/**/*"]
}