mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
be4a4d74d3
commit
b9e2fdb6a5
21 changed files with 635 additions and 151 deletions
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
noIntegrationsLabel,
|
||||
} from '../constants';
|
||||
|
||||
interface IntegrationsListStatusProps {
|
||||
export interface IntegrationsListStatusProps {
|
||||
integrations: Integration[] | null;
|
||||
error: Error | null;
|
||||
onRetry: ReloadIntegrations;
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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} />,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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';
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/io-ts-utils",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/rison",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue