Skip to content

Commit

Permalink
[VisBuilder Integration] Add initial setup and migrate state management
Browse files Browse the repository at this point in the history
Signed-off-by: ananzh <[email protected]>
  • Loading branch information
ananzh committed Nov 16, 2023
1 parent 7def75c commit ccf563a
Show file tree
Hide file tree
Showing 98 changed files with 4,757 additions and 12 deletions.
10 changes: 9 additions & 1 deletion src/plugins/data_explorer/public/services/view_service/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ export interface ViewDefinition<T = any> {
readonly title: string;
readonly ui?: {
defaults: DefaultViewState | (() => DefaultViewState) | (() => Promise<DefaultViewState>);
slice: Slice<T>;
slices: Array<Slice<T>>;
sideEffects?: Array<
(
store: Store,
currentState: RootState,
previousState?: RootState,
services?: DataExplorerServices
) => void
>;
};
readonly Canvas: LazyExoticComponent<(props: ViewProps) => React.ReactElement>;
readonly Panel: LazyExoticComponent<(props: ViewProps) => React.ReactElement>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ export const getPreloadedState = async (
return;
}

const { defaults } = view.ui;
const { defaults, slices } = view.ui;

try {
// defaults can be a function or an object
const preloadedState = typeof defaults === 'function' ? await defaults() : defaults;
rootState[view.id] = preloadedState.state;
slices.forEach((slice) => {
const id = slice.name;
rootState[id] = preloadedState.state.id ? preloadedState.state.id : preloadedState.state;
});

// if the view wants to override the root state, we do that here
if (preloadedState.root) {
Expand Down
35 changes: 27 additions & 8 deletions src/plugins/data_explorer/public/utils/state_management/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,18 @@ export const configurePreloadedStore = (preloadedState: PreloadedState<RootState
export const getPreloadedStore = async (services: DataExplorerServices) => {
// For each view preload the data and register the slice
const views = services.viewRegistry.all();
const viewSideEffectsMap: Record<string, Function[]> = {};

views.forEach((view) => {
if (!view.ui) return;

const { slice } = view.ui;
registerSlice(slice);
const { slices, sideEffects } = view.ui;
registerSlices(slices);

// Save side effects if they exist
if (sideEffects) {
viewSideEffectsMap[view.id] = sideEffects;
}
});

const preloadedState = await loadReduxState(services);
Expand All @@ -72,7 +79,17 @@ export const getPreloadedStore = async (services: DataExplorerServices) => {

if (isEqual(state, previousState)) return;

// Add Side effects here to apply after changes to the store are made. None for now.
// Execute view-specific side effects.
Object.entries(viewSideEffectsMap).forEach(([viewId, effects]) => {
effects.forEach((effect) => {
try {
effect(state, previousState, services);
} catch (e) {
// eslint-disable-next-line no-console
console.error(`Error executing side effect for view ${viewId}:`, e);
}
});
});

previousState = state;
};
Expand Down Expand Up @@ -103,11 +120,13 @@ export const getPreloadedStore = async (services: DataExplorerServices) => {
return { store, unsubscribe: onUnsubscribe };
};

export const registerSlice = (slice: Slice) => {
if (dynamicReducers[slice.name]) {
throw new Error(`Slice ${slice.name} already registered`);
}
dynamicReducers[slice.name] = slice.reducer;
export const registerSlices = (slices: Slice[]) => {
slices.forEach((slice) => {
if (dynamicReducers[slice.name]) {
throw new Error(`Slice ${slice.name} already registered`);
}
dynamicReducers[slice.name] = slice.reducer;
});
};

// Infer the `RootState` and `AppDispatch` types from the store itself
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/discover/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export class DiscoverPlugin
const services = getServices();
return await getPreloadedState(services);
},
slice: discoverSlice,
slices: [discoverSlice],
},
shouldShow: () => true,
// ViewComponent
Expand Down
15 changes: 15 additions & 0 deletions src/plugins/vis_builder_new/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export const PLUGIN_ID = 'vis-builder-new';
export const PLUGIN_NAME = 'VisBuilderNew';
export const VISUALIZE_ID = 'visualize';
export const EDIT_PATH = '/edit';
export const VIS_BUILDER_CHART_TYPE = 'VisBuilderNew';

export {
VisBuilderSavedObjectAttributes,
VISBUILDER_SAVED_OBJECT,
} from './vis_builder_saved_object_attributes';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { SavedObjectAttributes } from '../../../core/types';

export const VISBUILDER_SAVED_OBJECT = 'visualization-visbuilder-new';

export interface VisBuilderSavedObjectAttributes extends SavedObjectAttributes {
title: string;
description?: string;
visualizationState?: string;
updated_at?: string;
styleState?: string;
uiState?: string;
version: number;
searchSourceFields?: {
index?: string;
};
}
12 changes: 12 additions & 0 deletions src/plugins/vis_builder_new/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { schema, TypeOf } from '@osd/config-schema';

export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
});

export type ConfigSchema = TypeOf<typeof configSchema>;
25 changes: 25 additions & 0 deletions src/plugins/vis_builder_new/opensearch_dashboards.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"id": "visBuilder-new",
"version": "1.0.0",
"opensearchDashboardsVersion": "opensearchDashboards",
"server": true,
"ui": true,
"requiredPlugins": [
"dashboard",
"data",
"embeddable",
"expressions",
"navigation",
"savedObjects",
"visualizations",
"uiActions",
"dataExplorer"
],
"requiredBundles": [
"charts",
"opensearchDashboardsReact",
"opensearchDashboardsUtils",
"visDefaultEditor",
"visTypeVislib"
]
}
12 changes: 12 additions & 0 deletions src/plugins/vis_builder_new/public/application/_util.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
@mixin scrollNavParent($template-row: none) {
display: grid;
min-height: 0;

@if $template-row != "none" {
grid-template-rows: $template-row;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
@import "@elastic/eui/src/global_styling/variables/header";
@import "@elastic/eui/src/global_styling/variables/form";

$osdHeaderOffset: $euiHeaderHeightCompensation;
$vbLeftNavWidth: 462px;
27 changes: 27 additions & 0 deletions src/plugins/vis_builder_new/public/application/app.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
@import "variables";

.vbLayout {
padding: 0;
display: grid;
grid-template:
"topNav topNav" min-content
"leftNav workspaceNav" 1fr / #{$vbLeftNavWidth} 1fr;
height: calc(100vh - #{$osdHeaderOffset});

&__resizeContainer {
min-height: 0;
background-color: $euiColorEmptyShade;
}

&__resizeButton {
transform: translateX(-$euiSizeM / 2);
}
}

.headerIsExpanded .vbLayout {
height: calc(100vh - #{$osdHeaderOffset * 2});
}
84 changes: 84 additions & 0 deletions src/plugins/vis_builder_new/public/application/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useEffect } from 'react';
import { I18nProvider } from '@osd/i18n/react';
import { EuiPage, EuiResizableContainer } from '@elastic/eui';
import { useLocation } from 'react-router-dom';
// import { DragDropProvider } from './utils/drag_drop/drag_drop_context';
// import { LeftNav } from './components/left_nav';
// import { TopNav } from './components/top_nav';
// import { Workspace } from './components/workspace';
// import { RightNav } from './components/right_nav';
import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public';
import { VisBuilderServices } from '../types';
import { syncQueryStateWithUrl } from '../../../data/public';

import './app.scss';

export const VisBuilderApp = () => {
const {
services: {
data: { query },
osdUrlStateStorage,
},
} = useOpenSearchDashboards<VisBuilderServices>();
const { pathname } = useLocation();

useEffect(() => {
// syncs `_g` portion of url with query services
const { stop } = syncQueryStateWithUrl(query, osdUrlStateStorage);

return () => stop();

// this effect should re-run when pathname is changed to preserve querystring part,
// so the global state is always preserved
}, [query, osdUrlStateStorage, pathname]);

// Render the application DOM.
return (
<I18nProvider>
{/* <DragDropProvider>
<EuiPage className="vbLayout">
<TopNav />
<LeftNav />
<EuiResizableContainer className="vbLayout__resizeContainer">
{(EuiResizablePanel, EuiResizableButton) => (
<>
<EuiResizablePanel
className="vbLayout__workspaceResize"
paddingSize="none"
initialSize={80}
minSize="300px"
mode="main"
>
<Workspace />
</EuiResizablePanel>
<EuiResizableButton className="vbLayout__resizeButton" />
<EuiResizablePanel
className="vbLayout__rightNavResize"
paddingSize="none"
initialSize={20}
minSize="250px"
mode={[
'collapsible',
{
position: 'top',
},
]}
id="vbRightResize"
>
<RightNav />
</EuiResizablePanel>
</>
)}
</EuiResizableContainer>
</EuiPage>
</DragDropProvider>*/}
</I18nProvider>
);
};

export { Option } from './components/option';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.vbOption {
background-color: $euiColorEmptyShade;
padding: $euiSizeM;

& &__panel {
background-color: $euiColorLightestShade;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui';
import React, { FC } from 'react';
import './option.scss';

interface Props {
title: string;
initialIsOpen?: boolean;
}

export const Option: FC<Props> = ({ title, children, initialIsOpen = false }) => {
return (
<>
<EuiAccordion
id={title}
buttonContent={title}
className="vbOption"
initialIsOpen={initialIsOpen}
data-test-subj={`vbOption-${title.replace(/\s+/g, '-')}`}
>
<EuiSpacer size="s" />
<EuiPanel color="subdued" className="vbOption__panel">
{children}
</EuiPanel>
</EuiAccordion>
<EuiHorizontalRule margin="none" />
</>
);
};
Loading

0 comments on commit ccf563a

Please sign in to comment.