Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] Add a component details side panel #58

Merged
merged 1 commit into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions public/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface Props extends RouteComponentProps {}

export const AiFlowDashboardsApp = (props: Props) => {
const sidebar = (
<EuiPageSideBar style={{ minWidth: 190 }} hidden={false}>
<EuiPageSideBar style={{ minWidth: 190 }} hidden={false} paddingSize="l">
<EuiSideNav
style={{ width: 190 }}
items={[
Expand Down Expand Up @@ -50,7 +50,10 @@ export const AiFlowDashboardsApp = (props: Props) => {
return (
<EuiPageTemplate
template="empty"
pageContentProps={{ paddingSize: 'm' }}
paddingSize="none"
grow={true}
restrictWidth={false}
pageContentProps={{ paddingSize: 's' }}
pageSideBar={sidebar}
>
<Switch>
Expand Down
8 changes: 8 additions & 0 deletions public/pages/overview/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ import {
import { BREADCRUMBS } from '../../utils';
import { getCore } from '../../services';

/**
* The overview page. This contains a detailed description on what
* this plugin offers, and links to different resources (blogs, demos,
* documentation, etc.)
*
* This may be hidden for the initial release until we have sufficient content
* such that this page adds enough utility & user value.
*/
export function Overview() {
useEffect(() => {
getCore().chrome.setBreadcrumbs([
Expand Down
9 changes: 7 additions & 2 deletions public/pages/workflow_detail/workflow_detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { EuiPage, EuiPageBody } from '@elastic/eui';
import { BREADCRUMBS } from '../../utils';
import { getCore } from '../../services';
import { WorkflowDetailHeader } from './components';
import { Workspace } from './workspace';
import { AppState } from '../../store';
import { ResizableWorkspace } from './workspace';

export interface WorkflowDetailRouterProps {
workflowId: string;
Expand All @@ -20,6 +20,11 @@ export interface WorkflowDetailRouterProps {
interface WorkflowDetailProps
extends RouteComponentProps<WorkflowDetailRouterProps> {}

/**
* The workflow details page. This is where users will configure, create, and
* test their created workflows. Additionally, can be used to load existing workflows
* to view details and/or make changes to them.
*/
export function WorkflowDetail(props: WorkflowDetailProps) {
const { workflows } = useSelector((state: AppState) => state.workflows);

Expand All @@ -40,7 +45,7 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
<EuiPage>
<EuiPageBody>
<WorkflowDetailHeader workflow={workflow} />
<Workspace workflow={workflow} />
<ResizableWorkspace workflow={workflow} />
</EuiPageBody>
</EuiPage>
);
Expand Down
99 changes: 99 additions & 0 deletions public/pages/workflow_detail/workspace/component_details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState, useContext } from 'react';
import { useOnSelectionChange } from 'reactflow';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiPanel,
EuiTitle,
EuiEmptyPrompt,
EuiText,
} from '@elastic/eui';
import { ReactFlowComponent } from '../../../../common';
import { rfContext } from '../../../store';
import { InputFieldList } from '../workspace_component/input_field_list';

// styling
import './workspace-styles.scss';

interface ComponentDetailsProps {
onToggleChange: () => void;
isOpen: boolean;
}

/**
* A panel that will be nested in a resizable container to dynamically show
* the details and user-required inputs based on the selected component
* in the flow workspace.
*/
export function ComponentDetails(props: ComponentDetailsProps) {
// TODO: use this instance to update the internal node state. ex: update field data in the selected node based
// on user input
const { reactFlowInstance } = useContext(rfContext);

const [selectedComponent, setSelectedComponent] = useState<
ReactFlowComponent
>();

/**
* Hook provided by reactflow to listen on when nodes are selected / de-selected.
* - populate panel content appropriately
* - open the panel if a node is selected and the panel is closed
* - it is assumed that only one node can be selected at once
*/
useOnSelectionChange({
onChange: ({ nodes, edges }) => {
if (nodes && nodes.length > 0) {
setSelectedComponent(nodes[0]);
if (!props.isOpen) {
props.onToggleChange();
}
} else {
setSelectedComponent(undefined);
}
},
});

return (
<EuiFlexGroup
direction="column"
gutterSize="none"
className="workspace-panel"
>
<EuiFlexItem className="resizable-panel-border">
<EuiPanel paddingSize="m">
{selectedComponent ? (
<>
<EuiTitle size="m">
<h2>{selectedComponent?.data.label || ''}</h2>
</EuiTitle>
<EuiSpacer size="s" />
<InputFieldList
inputFields={selectedComponent?.data.fields || []}
/>
</>
) : (
<EuiEmptyPrompt
iconType={'cross'}
title={<h2>No component selected</h2>}
titleSize="s"
body={
<>
<EuiText>
Add a component, or select a component to view or edit its
configuration.
</EuiText>
</>
}
/>
)}
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
);
}
2 changes: 1 addition & 1 deletion public/pages/workflow_detail/workspace/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
* SPDX-License-Identifier: Apache-2.0
*/

export { Workspace } from './workspace';
export { ResizableWorkspace } from './resizable_workspace';
16 changes: 7 additions & 9 deletions public/pages/workflow_detail/workspace/reactflow-styles.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
$handle-color: #5a5a5a;
$handle-color-valid: #55dd99;
$handle-color-invalid: #ff6060;
$handle-color: $euiColorDarkestShade;
$handle-color-valid: $euiColorSuccess;
$handle-color-invalid: $euiColorDanger;

.reactflow-workspace {
background: $euiColorEmptyShade;
}

.reactflow-parent-wrapper {
display: flex;
Expand All @@ -13,12 +17,6 @@ $handle-color-invalid: #ff6060;
height: 100%;
}

.workspace {
width: 80vh;
height: 50vh;
padding: 0;
}

.reactflow-workspace .react-flow__node {
width: 300px;
}
Expand Down
68 changes: 68 additions & 0 deletions public/pages/workflow_detail/workspace/resizable_workspace.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useRef, useState } from 'react';
import { ReactFlowProvider } from 'reactflow';
import { EuiResizableContainer } from '@elastic/eui';
import { Workflow } from '../../../../common';
import { Workspace } from './workspace';
import { ComponentDetails } from './component_details';

interface ResizableWorkspaceProps {
workflow?: Workflow;
}

const COMPONENT_DETAILS_PANEL_ID = 'component_details_panel_id';

/**
* The overall workspace component that maintains state related to the 2 resizable
* panels - the ReactFlow workspace panel and the selected component details panel.
*/
export function ResizableWorkspace(props: ResizableWorkspaceProps) {
const [isOpen, setIsOpen] = useState<boolean>(true);
const collapseFn = useRef(
(id: string, options: { direction: 'left' | 'right' }) => {}
);

const onToggleChange = () => {
collapseFn.current(COMPONENT_DETAILS_PANEL_ID, { direction: 'left' });
setIsOpen(!isOpen);
};

return (
<EuiResizableContainer
direction="horizontal"
style={{ marginLeft: '-14px' }}
>
{(EuiResizablePanel, EuiResizableButton, { togglePanel }) => {
if (togglePanel) {
collapseFn.current = (panelId: string, { direction }) =>
togglePanel(panelId, { direction });
}

return (
<ReactFlowProvider>
<EuiResizablePanel mode="main" initialSize={75} minSize="50%">
<Workspace workflow={props.workflow} />
</EuiResizablePanel>
<EuiResizableButton />
<EuiResizablePanel
id={COMPONENT_DETAILS_PANEL_ID}
mode="collapsible"
initialSize={25}
minSize="10%"
onToggleCollapsedInternal={() => onToggleChange()}
>
<ComponentDetails
onToggleChange={onToggleChange}
isOpen={isOpen}
/>
</EuiResizablePanel>
</ReactFlowProvider>
);
}}
</EuiResizableContainer>
);
}
12 changes: 12 additions & 0 deletions public/pages/workflow_detail/workspace/workspace-styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.workspace-panel {
// this ratio will allow the workspace to render on a standard
// laptop (3024 x 1964) without introducing overflow/scrolling
height: 60vh;
padding: 0;
}

.resizable-panel-border {
border-style: groove;
border-color: gray;
border-width: 1px;
}
19 changes: 9 additions & 10 deletions public/pages/workflow_detail/workspace/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ReactFlow, {
useNodesState,
useEdgesState,
addEdge,
BackgroundVariant,
} from 'reactflow';
import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { rfContext, setDirty } from '../../../store';
Expand All @@ -23,6 +24,7 @@ import { DeletableEdge } from '../workspace_edge';
// styling
import 'reactflow/dist/style.css';
import './reactflow-styles.scss';
import './workspace-styles.scss';
import '../workspace_edge/deletable-edge-styles.scss';

interface WorkspaceProps {
Expand Down Expand Up @@ -117,17 +119,11 @@ export function Workspace(props: WorkspaceProps) {
return (
<EuiFlexGroup
direction="column"
gutterSize="m"
gutterSize="none"
justifyContent="spaceBetween"
className="workspace"
className="workspace-panel"
>
<EuiFlexItem
style={{
borderStyle: 'groove',
borderColor: 'gray',
borderWidth: '1px',
}}
>
<EuiFlexItem className="resizable-panel-border">
{/**
* We have these wrapper divs & reactFlowWrapper ref to control and calculate the
* ReactFlow bounds when calculating node positioning.
Expand All @@ -149,7 +145,10 @@ export function Workspace(props: WorkspaceProps) {
fitView
>
<Controls />
<Background />
<Background
color="#343741"
variant={'dots' as BackgroundVariant}
/>
</ReactFlow>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface InputFieldListProps {

export function InputFieldList(props: InputFieldListProps) {
return (
<EuiFlexItem>
<EuiFlexItem grow={false}>
{props.inputFields?.map((field, idx) => {
let el;
switch (field.type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState } from 'react';
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiCard } from '@elastic/eui';
import { IComponent } from '../../../component_types';
import { InputFieldList } from './input_field_list';
import { NewOrExistingTabs } from './new_or_existing_tabs';
import { InputHandle } from './input_handle';
import { OutputHandle } from './output_handle';

Expand All @@ -23,32 +21,17 @@ interface WorkspaceComponentProps {
export function WorkspaceComponent(props: WorkspaceComponentProps) {
const component = props.data;

const [selectedTabId, setSelectedTabId] = useState<string>('existing');

const isCreatingNew = component.allowsCreation && selectedTabId === 'new';
const fieldsToDisplay = isCreatingNew
? component.createFields
: component.fields;

return (
<EuiCard title={component.label}>
<EuiFlexGroup direction="column">
{/* <EuiFlexItem>
{component.allowsCreation ? (
<NewOrExistingTabs
setSelectedTabId={setSelectedTabId}
selectedTabId={selectedTabId}
/>
) : undefined}
</EuiFlexItem> */}
{component.inputs?.map((input, index) => {
return (
<EuiFlexItem key={index}>
<InputHandle input={input} data={component} />
</EuiFlexItem>
);
})}
<InputFieldList inputFields={fieldsToDisplay} />
{/* TODO: finalize from UX what we show in the component itself. Readonly fields? Configure in the component JSON definition? */}
{component.outputs?.map((output, index) => {
return (
<EuiFlexItem key={index}>
Expand Down
Loading
Loading