/* * 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 { IconType } from '@elastic/eui/src/components/icon/icon'; import { CoreSetup } from 'kibana/public'; import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; import { SavedObjectReference } from 'kibana/public'; import { RowClickContext } from '../../../../src/plugins/ui_actions/public'; import { ExpressionAstExpression, ExpressionRendererEvent, IInterpreterRenderHandlers, Datatable, SerializedFieldFormat, } from '../../../../src/plugins/expressions/public'; import { DragContextState, DragDropIdentifier } from './drag_drop'; import { Document } from './persistence'; import { DateRange } from '../common'; import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public'; import { VisualizeFieldContext } from '../../../../src/plugins/ui_actions/public'; import { RangeSelectContext, ValueClickContext } from '../../../../src/plugins/embeddable/public'; import { LENS_EDIT_SORT_ACTION, LENS_EDIT_RESIZE_ACTION, LENS_TOGGLE_ACTION, } from './datatable_visualization/components/constants'; import type { LensSortActionData, LensResizeActionData, LensToggleActionData, } from './datatable_visualization/components/types'; export type ErrorCallback = (e: { message: string }) => void; export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; export interface PublicAPIProps { state: T; layerId: string; } export interface EditorFrameProps { onError: ErrorCallback; doc?: Document; dateRange: DateRange; query: Query; filters: Filter[]; savedQuery?: SavedQuery; searchSessionId: string; initialContext?: VisualizeFieldContext; // Frame loader (app or embeddable) is expected to call this when it loads and updates // This should be replaced with a top-down state onChange: (newState: { filterableIndexPatterns: string[]; doc: Document; isSaveable: boolean; activeData?: Record; }) => void; showNoDataPopover: () => void; } export interface EditorFrameInstance { mount: (element: Element, props: EditorFrameProps) => void; unmount: () => void; } export interface EditorFrameSetup { // generic type on the API functions to pull the "unknown vs. specific type" error into the implementation registerDatasource: ( datasource: Datasource | (() => Promise>) ) => void; registerVisualization: ( visualization: Visualization | (() => Promise>) ) => void; } export interface EditorFrameStart { createInstance: () => Promise; } export interface TableSuggestionColumn { columnId: string; operation: Operation; } /** * A possible table a datasource can create. This object is passed to the visualization * which tries to build a meaningful visualization given the shape of the table. If this * is possible, the visualization returns a `VisualizationSuggestion` object */ export interface TableSuggestion { /** * Flag indicating whether the table will include more than one column. * This is not the case for example for a single metric aggregation * */ isMultiRow: boolean; /** * The columns of the table. Each column has to be mapped to a dimension in a chart. If a visualization * can't use all columns of a suggestion, it should not return a `VisualizationSuggestion` based on it * because there would be unreferenced columns */ columns: TableSuggestionColumn[]; /** * The layer this table will replace. This is only relevant if the visualization this suggestion is passed * is currently active and has multiple layers configured. If this suggestion is applied, the table of this * layer will be replaced by the columns specified in this suggestion */ layerId: string; /** * A label describing the table. This can be used to provide a title for the `VisualizationSuggestion`, * but the visualization can also decide to overwrite it. */ label?: string; /** * The change type indicates what was changed in this table compared to the currently active table of this layer. */ changeType: TableChangeType; } /** * Indicates what was changed in this table compared to the currently active table of this layer. * * `initial` means the layer associated with this table does not exist in the current configuration * * `unchanged` means the table is the same in the currently active configuration * * `reduced` means the table is a reduced version of the currently active table (some columns dropped, but not all of them) * * `extended` means the table is an extended version of the currently active table (added one or multiple additional columns) * * `reorder` means the table columns have changed order, which change the data as well * * `layers` means the change is a change to the layer structure, not to the table */ export type TableChangeType = | 'initial' | 'unchanged' | 'reduced' | 'extended' | 'reorder' | 'layers'; export type DropType = | 'field_add' | 'field_replace' | 'reorder' | 'duplicate_in_group' | 'move_compatible' | 'replace_compatible' | 'move_incompatible' | 'replace_incompatible'; export interface DatasourceSuggestion { state: T; table: TableSuggestion; keptLayerIds: string[]; } export type StateSetter = (newState: T | ((prevState: T) => T)) => void; export interface InitializationOptions { isFullEditor?: boolean; } /** * Interface for the datasource registry */ export interface Datasource { id: string; // For initializing, either from an empty state or from persisted state // Because this will be called at runtime, state might have a type of `any` and // datasources should validate their arguments initialize: ( state?: P, savedObjectReferences?: SavedObjectReference[], initialContext?: VisualizeFieldContext, options?: InitializationOptions ) => Promise; // Given the current state, which parts should be saved? getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; insertLayer: (state: T, newLayerId: string) => T; removeLayer: (state: T, layerId: string) => T; clearLayer: (state: T, layerId: string) => T; getLayers: (state: T) => string[]; removeColumn: (props: { prevState: T; layerId: string; columnId: string }) => T; renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; getDropProps: ( props: DatasourceDimensionDropProps & { groupId: string; dragging: DragContextState['dragging']; } ) => { dropType: DropType; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; updateStateOnCloseDimension?: (props: { layerId: string; columnId: string; state: T; }) => T | undefined; toExpression: (state: T, layerId: string) => ExpressionAstExpression | string | null; getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>; getDatasourceSuggestionsForVisualizeField: ( state: T, indexPatternId: string, fieldName: string ) => Array>; getDatasourceSuggestionsFromCurrentState: ( state: T, activeData?: Record ) => Array>; getPublicAPI: (props: PublicAPIProps) => DatasourcePublicAPI; getErrorMessages: ( state: T, layersGroups?: Record ) => Array<{ shortMessage: string; longMessage: string }> | undefined; /** * uniqueLabels of dimensions exposed for aria-labels of dragged dimensions */ uniqueLabels: (state: T) => Record; } /** * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource */ export interface DatasourcePublicAPI { datasourceId: string; getTableSpec: () => Array<{ columnId: string }>; getOperationForColumnId: (columnId: string) => Operation | null; } export interface DatasourceDataPanelProps { state: T; dragDropContext: DragContextState; setState: StateSetter; showNoDataPopover: () => void; core: Pick; query: Query; dateRange: DateRange; filters: Filter[]; dropOntoWorkspace: (field: DragDropIdentifier) => void; hasSuggestionForField: (field: DragDropIdentifier) => boolean; } interface SharedDimensionProps { /** Visualizations can restrict operations based on their own rules. * For example, limiting to only bucketed or only numeric operations. */ filterOperations: (operation: OperationMetadata) => boolean; /** Some dimension editors will allow users to change the operation grouping * from the panel, and this lets the visualization hint that it doesn't want * users to have that level of control */ hideGrouping?: boolean; } export type DatasourceDimensionProps = SharedDimensionProps & { layerId: string; columnId: string; onRemove?: (accessor: string) => void; state: T; activeData?: Record; }; // The only way a visualization has to restrict the query building export type DatasourceDimensionEditorProps = DatasourceDimensionProps & { // Not a StateSetter because we have this unique use case of determining valid columns setState: ( newState: Parameters>[0], publishToVisualization?: { shouldReplaceDimension?: boolean; shouldRemoveDimension?: boolean } ) => void; core: Pick; dateRange: DateRange; dimensionGroups: VisualizationDimensionGroupConfig[]; }; export type DatasourceDimensionTriggerProps = DatasourceDimensionProps; export interface DatasourceLayerPanelProps { layerId: string; state: T; setState: StateSetter; activeData?: Record; } export interface DraggedOperation { layerId: string; groupId: string; columnId: string; } export function isDraggedOperation( operationCandidate: unknown ): operationCandidate is DraggedOperation { return ( typeof operationCandidate === 'object' && operationCandidate !== null && 'columnId' in operationCandidate ); } export type DatasourceDimensionDropProps = SharedDimensionProps & { layerId: string; columnId: string; state: T; setState: StateSetter; }; export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { droppedItem: unknown; dropType: DropType; }; export type FieldOnlyDataType = 'document' | 'ip' | 'histogram'; export type DataType = 'string' | 'number' | 'date' | 'boolean' | FieldOnlyDataType; // An operation represents a column in a table, not any information // about how the column was created such as whether it is a sum or average. // Visualizations are able to filter based on the output, not based on the // underlying data export interface Operation extends OperationMetadata { // User-facing label for the operation label: string; } export interface OperationMetadata { // The output of this operation will have this data type dataType: DataType; // A bucketed operation is grouped by duplicate values, otherwise each row is // treated as unique isBucketed: boolean; /** * ordinal: Each name is a unique value, but the names are in sorted order, like "Top values" * interval: Histogram data, like date or number histograms * ratio: Most number data is rendered as a ratio that includes 0 */ scale?: 'ordinal' | 'interval' | 'ratio'; // Extra meta-information like cardinality, color // TODO currently it's not possible to differentiate between a field from a raw // document and an aggregated metric which might be handy in some cases. Once we // introduce a raw document datasource, this should be considered here. } export interface LensMultiTable { type: 'lens_multitable'; tables: Record; dateRange?: { fromDate: Date; toDate: Date; }; } export interface VisualizationConfigProps { layerId: string; frame: Pick; state: T; } export type VisualizationLayerWidgetProps = VisualizationConfigProps & { setState: (newState: T) => void; }; export interface VisualizationToolbarProps { setState: (newState: T) => void; frame: FramePublicAPI; state: T; } export type VisualizationDimensionEditorProps = VisualizationConfigProps & { groupId: string; accessor: string; setState: (newState: T) => void; }; export interface AccessorConfig { columnId: string; triggerIcon?: 'color' | 'disabled' | 'colorBy' | 'none' | 'invisible'; color?: string; palette?: string[]; } export type VisualizationDimensionGroupConfig = SharedDimensionProps & { groupLabel: string; /** ID is passed back to visualization. For example, `x` */ groupId: string; accessors: AccessorConfig[]; supportsMoreColumns: boolean; /** If required, a warning will appear if accessors are empty */ required?: boolean; dataTestSubj?: string; /** * When the dimension editor is enabled for this group, all dimensions in the group * will render the extra tab for the dimension editor */ enableDimensionEditor?: boolean; }; interface VisualizationDimensionChangeProps { layerId: string; columnId: string; prevState: T; } /** * Object passed to `getSuggestions` of a visualization. * It contains a possible table the current datasource could * provide and the state of the visualization if it is currently active. * * If the current datasource suggests multiple tables, `getSuggestions` * is called multiple times with separate `SuggestionRequest` objects. */ export interface SuggestionRequest { /** * A table configuration the datasource could provide. */ table: TableSuggestion; /** * State is only passed if the visualization is active. */ state?: T; mainPalette?: PaletteOutput; /** * The visualization needs to know which table is being suggested */ keptLayerIds: string[]; /** * Different suggestions can be generated for each subtype of the visualization */ subVisualizationId?: string; } /** * A possible configuration of a given visualization. It is based on a `TableSuggestion`. * Suggestion might be shown in the UI to be chosen by the user directly, but they are * also applied directly under some circumstances (dragging in the first field from the data * panel or switching to another visualization in the chart switcher). */ export interface VisualizationSuggestion { /** * The score of a suggestion should indicate how valuable the suggestion is. It is used * to rank multiple suggestions of multiple visualizations. The number should be between 0 and 1 */ score: number; /** * Flag indicating whether this suggestion should not be advertised to the user. It is still * considered in scenarios where the available suggestion with the highest suggestion is applied * directly. */ hide?: boolean; /** * Descriptive title of the suggestion. Should be as short as possible. This title is shown if * the suggestion is advertised to the user and will also show either the `previewExpression` or * the `previewIcon` */ title: string; /** * The new state of the visualization if this suggestion is applied. */ state: T; /** * An EUI icon type shown instead of the preview expression. */ previewIcon: IconType; } export interface FramePublicAPI { datasourceLayers: Record; /** * Data of the chart currently rendered in the preview. * This data might be not available (e.g. if the chart can't be rendered) or outdated and belonging to another chart. * If accessing, make sure to check whether expected columns actually exist. */ activeData?: Record; dateRange: DateRange; query: Query; filters: Filter[]; searchSessionId: string; /** * A map of all available palettes (keys being the ids). */ availablePalettes: PaletteRegistry; // Adds a new layer. This has a side effect of updating the datasource state addNewLayer: () => string; removeLayers: (layerIds: string[]) => void; } /** * A visualization type advertised to the user in the chart switcher */ export interface VisualizationType { /** * Unique id of the visualization type within the visualization defining it */ id: string; /** * Icon used in the chart switcher */ icon: IconType; /** * Visible label used in the chart switcher and above the workspace panel in collapsed state */ label: string; /** * Optional label used in chart type search if chart switcher is expanded and for tooltips */ fullLabel?: string; } export interface Visualization { /** Plugin ID, such as "lnsXY" */ id: string; /** * Initialize is allowed to modify the state stored in memory. The initialize function * is called with a previous state in two cases: * - Loadingn from a saved visualization * - When using suggestions, the suggested state is passed in */ initialize: (frame: FramePublicAPI, state?: T, mainPalette?: PaletteOutput) => T; getMainPalette?: (state: T) => undefined | PaletteOutput; /** * Visualizations must provide at least one type for the chart switcher, * but can register multiple subtypes */ visualizationTypes: VisualizationType[]; /** * Return the ID of the current visualization. Used to highlight * the active subtype of the visualization. */ getVisualizationTypeId: (state: T) => string; /** * If the visualization has subtypes, update the subtype in state. */ switchVisualizationType?: (visualizationTypeId: string, state: T) => T; /** Description is displayed as the clickable text in the chart switcher */ getDescription: (state: T) => { icon?: IconType; label: string }; /** Frame needs to know which layers the visualization is currently using */ getLayerIds: (state: T) => string[]; /** Reset button on each layer triggers this */ clearLayer: (state: T, layerId: string) => T; /** Optional, if the visualization supports multiple layers */ removeLayer?: (state: T, layerId: string) => T; /** Track added layers in internal state */ appendLayer?: (state: T, layerId: string) => T; /** * For consistency across different visualizations, the dimension configuration UI is standardized */ getConfiguration: ( props: VisualizationConfigProps ) => { groups: VisualizationDimensionGroupConfig[] }; /** * Popover contents that open when the user clicks the contextMenuIcon. This can be used * for extra configurability, such as for styling the legend or axis */ renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; /** * Toolbar rendered above the visualization. This is meant to be used to provide chart-level * settings for the visualization. */ renderToolbar?: (domElement: Element, props: VisualizationToolbarProps) => void; /** * Visualizations can provide a custom icon which will open a layer-specific popover * If no icon is provided, gear icon is default */ getLayerContextMenuIcon?: (opts: { state: T; layerId: string; }) => { icon: IconType | 'gear'; label: string } | undefined; /** * The frame is telling the visualization to update or set a dimension based on user interaction * groupId is coming from the groupId provided in getConfiguration */ setDimension: (props: VisualizationDimensionChangeProps & { groupId: string }) => T; /** * The frame is telling the visualization to remove a dimension. The visualization needs to * look at its internal state to determine which dimension is being affected. */ removeDimension: (props: VisualizationDimensionChangeProps) => T; /** * Additional editor that gets rendered inside the dimension popover. * This can be used to configure dimension-specific options */ renderDimensionEditor?: ( domElement: Element, props: VisualizationDimensionEditorProps ) => void; /** * The frame will call this function on all visualizations at different times. The * main use cases where visualization suggestions are requested are: * - When dragging a field * - When opening the chart switcher * If the state is provided when requesting suggestions, the visualization is active. * Most visualizations will apply stricter filtering to suggestions when they are active, * because suggestions have the potential to remove the users's work in progress. */ getSuggestions: (context: SuggestionRequest) => Array>; toExpression: ( state: T, datasourceLayers: Record, attributes?: Partial<{ title: string; description: string }> ) => ExpressionAstExpression | string | null; /** * Expression to render a preview version of the chart in very constrained space. * If there is no expression provided, the preview icon is used. */ toPreviewExpression?: ( state: T, datasourceLayers: Record ) => ExpressionAstExpression | string | null; /** * The frame will call this function on all visualizations at few stages (pre-build/build error) in order * to provide more context to the error and show it to the user */ getErrorMessages: (state: T) => Array<{ shortMessage: string; longMessage: string }> | undefined; /** * The frame calls this function to display warnings about visualization */ getWarningMessages?: (state: T, frame: FramePublicAPI) => React.ReactNode[] | undefined; /** * On Edit events the frame will call this to know what's going to be the next visualization state */ onEditAction?: (state: T, event: LensEditEvent) => T; } export interface LensFilterEvent { name: 'filter'; data: ValueClickContext['data']; } export interface LensBrushEvent { name: 'brush'; data: RangeSelectContext['data']; } // Use same technique as TriggerContext interface LensEditContextMapping { [LENS_EDIT_SORT_ACTION]: LensSortActionData; [LENS_EDIT_RESIZE_ACTION]: LensResizeActionData; [LENS_TOGGLE_ACTION]: LensToggleActionData; } type LensEditSupportedActions = keyof LensEditContextMapping; export type LensEditPayload = { action: T; } & LensEditContextMapping[T]; type EditPayloadContext = T extends LensEditSupportedActions ? LensEditPayload : never; export interface LensEditEvent { name: 'edit'; data: EditPayloadContext; } export interface LensTableRowContextMenuEvent { name: 'tableRowContextMenuClick'; data: RowClickContext['data']; } export function isLensFilterEvent(event: ExpressionRendererEvent): event is LensFilterEvent { return event.name === 'filter'; } export function isLensBrushEvent(event: ExpressionRendererEvent): event is LensBrushEvent { return event.name === 'brush'; } export function isLensEditEvent( event: ExpressionRendererEvent ): event is LensEditEvent { return event.name === 'edit'; } export function isLensTableRowContextMenuClickEvent( event: ExpressionRendererEvent ): event is LensBrushEvent { return event.name === 'tableRowContextMenuClick'; } /** * Expression renderer handlers specifically for lens renderers. This is a narrowed down * version of the general render handlers, specifying supported event types. If this type is * used, dispatched events will be handled correctly. */ export interface ILensInterpreterRenderHandlers extends IInterpreterRenderHandlers { event: ( event: | LensFilterEvent | LensBrushEvent | LensEditEvent | LensTableRowContextMenuEvent ) => void; }