From 4e80e73019164588f0426e4ea1afaa1c6fe1e107 Mon Sep 17 00:00:00 2001
From: Tyler Ohlsen <ohltyler@amazon.com>
Date: Fri, 1 Mar 2024 10:09:12 -0800
Subject: [PATCH 1/4] Add preset workflow library stub; add search & filtering

Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
---
 common/interfaces.ts                          | 14 ++--
 .../workflows/new_workflow/new_workflow.tsx   | 76 ++++++++++++++-----
 .../pages/workflows/new_workflow/presets.tsx  | 59 ++++++++++++++
 .../pages/workflows/new_workflow/use_case.tsx |  1 +
 .../workflows/workflow_list/workflow_list.tsx |  2 +-
 5 files changed, 128 insertions(+), 24 deletions(-)
 create mode 100644 public/pages/workflows/new_workflow/presets.tsx

diff --git a/common/interfaces.ts b/common/interfaces.ts
index a493d650..a927f0e8 100644
--- a/common/interfaces.ts
+++ b/common/interfaces.ts
@@ -69,16 +69,20 @@ export type UseCaseTemplate = {
 };
 
 export type Workflow = {
-  id: string;
+  // won't exist until created in backend
+  id?: string;
   name: string;
   useCase: string;
+  template: UseCaseTemplate;
   description?: string;
   // ReactFlow state may not exist if a workflow is created via API/backend-only.
   workspaceFlowState?: WorkspaceFlowState;
-  template: UseCaseTemplate;
-  lastUpdated: number;
-  lastLaunched: number;
-  state: WORKFLOW_STATE;
+  // won't exist until created in backend
+  lastUpdated?: number;
+  // won't exist until launched/provisioned in backend
+  lastLaunched?: number;
+  // won't exist until launched/provisioned in backend
+  state?: WORKFLOW_STATE;
 };
 
 export enum USE_CASE {
diff --git a/public/pages/workflows/new_workflow/new_workflow.tsx b/public/pages/workflows/new_workflow/new_workflow.tsx
index ce87c443..d5644210 100644
--- a/public/pages/workflows/new_workflow/new_workflow.tsx
+++ b/public/pages/workflows/new_workflow/new_workflow.tsx
@@ -3,10 +3,18 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-import React from 'react';
-import { EuiFlexItem, EuiFlexGrid } from '@elastic/eui';
+import React, { useState, useEffect } from 'react';
+import { debounce } from 'lodash';
+import {
+  EuiFlexItem,
+  EuiFlexGrid,
+  EuiFlexGroup,
+  EuiFieldSearch,
+} from '@elastic/eui';
 
 import { UseCase } from './use_case';
+import { getPresetWorkflows } from './presets';
+import { Workflow } from '../../../../common';
 
 interface NewWorkflowProps {}
 
@@ -18,26 +26,58 @@ interface NewWorkflowProps {}
  * workflow for users to start with.
  */
 export function NewWorkflow(props: NewWorkflowProps) {
+  // preset workflow state
+  const presetWorkflows = getPresetWorkflows();
+  const [filteredWorkflows, setFilteredWorkflows] = useState<Workflow[]>(
+    getPresetWorkflows()
+  );
+
+  // search bar state
+  const [searchQuery, setSearchQuery] = useState<string>('');
+  const debounceSearchQuery = debounce((query: string) => {
+    setSearchQuery(query);
+  }, 200);
+
+  // When search query updated, re-filter preset list
+  useEffect(() => {
+    setFilteredWorkflows(fetchFilteredWorkflows(presetWorkflows, searchQuery));
+  }, [searchQuery]);
+
   return (
-    <EuiFlexGrid columns={3} gutterSize="l">
-      <EuiFlexItem>
-        <UseCase
-          title="Semantic Search"
-          description="Semantic search description..."
+    <EuiFlexGroup direction="column">
+      <EuiFlexItem grow={true}>
+        <EuiFieldSearch
+          fullWidth={true}
+          placeholder="Search"
+          onChange={(e) => debounceSearchQuery(e.target.value)}
         />
       </EuiFlexItem>
       <EuiFlexItem>
-        <UseCase
-          title="Multi-modal Search"
-          description="Multi-modal search description..."
-        />
+        <EuiFlexGrid columns={3} gutterSize="l">
+          {filteredWorkflows.map((workflow: Workflow) => {
+            return (
+              <EuiFlexItem>
+                <UseCase
+                  title={workflow.name}
+                  description={workflow.description || ''}
+                />
+              </EuiFlexItem>
+            );
+          })}
+        </EuiFlexGrid>
       </EuiFlexItem>
-      <EuiFlexItem>
-        <UseCase
-          title="Search Summarization"
-          description="Search summarization description..."
-        />
-      </EuiFlexItem>
-    </EuiFlexGrid>
+    </EuiFlexGroup>
   );
 }
+
+// Collect the final preset workflow list after applying all filters
+function fetchFilteredWorkflows(
+  allWorkflows: Workflow[],
+  searchQuery: string
+): Workflow[] {
+  return searchQuery.length === 0
+    ? allWorkflows
+    : allWorkflows.filter((workflow) =>
+        workflow.name.toLowerCase().includes(searchQuery.toLowerCase())
+      );
+}
diff --git a/public/pages/workflows/new_workflow/presets.tsx b/public/pages/workflows/new_workflow/presets.tsx
new file mode 100644
index 00000000..2950a005
--- /dev/null
+++ b/public/pages/workflows/new_workflow/presets.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Workflow, WorkspaceFlowState } from '../../../../common';
+
+// TODO: fetch from the backend when the workflow library is complete.
+/**
+ * Used to fetch the library of preset workflows to provide to users.
+ */
+export function getPresetWorkflows(): Workflow[] {
+  return [
+    {
+      name: 'Semantic Search',
+      description:
+        'This semantic search workflow includes the essential ingestion and search pipelines that covers the most common search use cases.',
+      useCase: 'SEMANTIC_SEARCH',
+      template: {},
+      workspaceFlowState: {
+        nodes: [],
+        edges: [],
+      } as WorkspaceFlowState,
+    },
+    {
+      name: 'Semantic Search with Reranking',
+      description:
+        'This semantic search workflow variation includes an ML processor to rerank fetched results.',
+      useCase: 'SEMANTIC_SEARCH_WITH_RERANK',
+      template: {},
+      workspaceFlowState: {
+        nodes: [],
+        edges: [],
+      } as WorkspaceFlowState,
+    },
+    {
+      name: 'Start From Scratch',
+      description:
+        'Build your workflow from scratch according to your specific use cases. Start by adding components for your ingest or query needs.',
+      useCase: '',
+      template: {},
+      workspaceFlowState: {
+        nodes: [],
+        edges: [],
+      } as WorkspaceFlowState,
+    },
+    {
+      name: 'Visual Search',
+      description:
+        'Build an application that will return results based on images.',
+      useCase: 'SEMANTIC_SEARCH',
+      template: {},
+      workspaceFlowState: {
+        nodes: [],
+        edges: [],
+      } as WorkspaceFlowState,
+    },
+  ] as Workflow[];
+}
diff --git a/public/pages/workflows/new_workflow/use_case.tsx b/public/pages/workflows/new_workflow/use_case.tsx
index c7094c9a..baacb201 100644
--- a/public/pages/workflows/new_workflow/use_case.tsx
+++ b/public/pages/workflows/new_workflow/use_case.tsx
@@ -28,6 +28,7 @@ export function UseCase(props: UseCaseProps) {
       }
       titleSize="s"
       paddingSize="l"
+      layout="horizontal"
     >
       <EuiFlexGroup direction="column" gutterSize="l">
         <EuiHorizontalRule size="full" margin="m" />
diff --git a/public/pages/workflows/workflow_list/workflow_list.tsx b/public/pages/workflows/workflow_list/workflow_list.tsx
index 3e5b4484..f122a2e0 100644
--- a/public/pages/workflows/workflow_list/workflow_list.tsx
+++ b/public/pages/workflows/workflow_list/workflow_list.tsx
@@ -109,7 +109,7 @@ export function WorkflowList(props: WorkflowListProps) {
             <EuiFlexItem grow={true}>
               <EuiFieldSearch
                 fullWidth={true}
-                placeholder="Search workflows..."
+                placeholder="Search"
                 onChange={(e) => debounceSearchQuery(e.target.value)}
               />
             </EuiFlexItem>

From 1793726de3cb28a42347e6c1b35915ad88cc8d04 Mon Sep 17 00:00:00 2001
From: Tyler Ohlsen <ohltyler@amazon.com>
Date: Fri, 1 Mar 2024 11:27:28 -0800
Subject: [PATCH 2/4] Add navigation; handle new workflow state on details page

Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
---
 common/constants.ts                           |  5 ++++
 .../workflow_detail/components/header.tsx     |  3 +-
 .../pages/workflow_detail/workflow_detail.tsx | 28 +++++++++++++++----
 .../workflows/new_workflow/new_workflow.tsx   | 27 ++++++++++++++++--
 .../pages/workflows/new_workflow/use_case.tsx |  8 ++++--
 public/store/reducers/workflows_reducer.ts    | 24 ++++++++++++++++
 6 files changed, 83 insertions(+), 12 deletions(-)

diff --git a/common/constants.ts b/common/constants.ts
index 0903f38c..fa890192 100644
--- a/common/constants.ts
+++ b/common/constants.ts
@@ -29,3 +29,8 @@ export const SEARCH_WORKFLOWS_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/se
 export const GET_WORKFLOW_STATE_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/state`;
 export const CREATE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/create`;
 export const DELETE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/delete`;
+
+/**
+ * MISCELLANEOUS
+ */
+export const NEW_WORKFLOW_ID_URL = 'new';
diff --git a/public/pages/workflow_detail/components/header.tsx b/public/pages/workflow_detail/components/header.tsx
index 987baeb5..3c165ee9 100644
--- a/public/pages/workflow_detail/components/header.tsx
+++ b/public/pages/workflow_detail/components/header.tsx
@@ -12,6 +12,7 @@ import { rfContext, AppState, removeDirty } from '../../../store';
 
 interface WorkflowDetailHeaderProps {
   tabs: any[];
+  formattedWorkflowName: string;
   workflow?: Workflow;
 }
 
@@ -22,7 +23,7 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
 
   return (
     <EuiPageHeader
-      pageTitle={props.workflow ? props.workflow.name : ''}
+      pageTitle={props.formattedWorkflowName}
       rightSideItems={[
         <EuiButton fill={false} onClick={() => {}}>
           Prototype
diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx
index e57bcc83..f7988d77 100644
--- a/public/pages/workflow_detail/workflow_detail.tsx
+++ b/public/pages/workflow_detail/workflow_detail.tsx
@@ -5,7 +5,7 @@
 
 import React, { useEffect, useState } from 'react';
 import { RouteComponentProps, useLocation } from 'react-router-dom';
-import { useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
 import { ReactFlowProvider } from 'reactflow';
 import queryString from 'query-string';
 import { EuiPage, EuiPageBody } from '@elastic/eui';
@@ -16,6 +16,7 @@ import { AppState } from '../../store';
 import { ResizableWorkspace } from './workspace';
 import { Launches } from './launches';
 import { Prototype } from './prototype';
+import { NEW_WORKFLOW_ID_URL } from '../../../common';
 
 export interface WorkflowDetailRouterProps {
   workflowId: string;
@@ -45,13 +46,26 @@ function replaceActiveTab(activeTab: string, props: WorkflowDetailProps) {
  * 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.
+ * New, unsaved workflows are cached in the redux store and displayed here.
  */
+
+// TODO: if exiting the page, or if saving, clear the cached workflow. Can use redux clearCachedWorkflow()
 export function WorkflowDetail(props: WorkflowDetailProps) {
-  const { workflows } = useSelector((state: AppState) => state.workflows);
+  const { workflows, cachedWorkflow } = useSelector(
+    (state: AppState) => state.workflows
+  );
 
-  const workflow = workflows[props.match?.params?.workflowId];
-  const workflowName = workflow ? workflow.name : '';
+  const isNewWorkflow = props.match?.params?.workflowId === NEW_WORKFLOW_ID_URL;
+  const workflow = isNewWorkflow
+    ? cachedWorkflow
+    : workflows[props.match?.params?.workflowId];
+  const workflowName = workflow
+    ? workflow.name
+    : isNewWorkflow && !cachedWorkflow
+    ? 'new_workflow'
+    : '';
 
+  // tab state
   const tabFromUrl = queryString.parse(useLocation().search)[
     ACTIVE_TAB_PARAM
   ] as WORKFLOW_DETAILS_TAB;
@@ -112,7 +126,11 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
     <ReactFlowProvider>
       <EuiPage>
         <EuiPageBody>
-          <WorkflowDetailHeader workflow={workflow} tabs={tabs} />
+          <WorkflowDetailHeader
+            workflow={workflow}
+            formattedWorkflowName={workflowName}
+            tabs={tabs}
+          />
           {selectedTabId === WORKFLOW_DETAILS_TAB.EDITOR && (
             <ResizableWorkspace workflow={workflow} />
           )}
diff --git a/public/pages/workflows/new_workflow/new_workflow.tsx b/public/pages/workflows/new_workflow/new_workflow.tsx
index d5644210..32215dd8 100644
--- a/public/pages/workflows/new_workflow/new_workflow.tsx
+++ b/public/pages/workflows/new_workflow/new_workflow.tsx
@@ -11,10 +11,11 @@ import {
   EuiFlexGroup,
   EuiFieldSearch,
 } from '@elastic/eui';
-
+import { useDispatch } from 'react-redux';
 import { UseCase } from './use_case';
 import { getPresetWorkflows } from './presets';
 import { Workflow } from '../../../../common';
+import { cacheWorkflow } from '../../../store';
 
 interface NewWorkflowProps {}
 
@@ -26,6 +27,7 @@ interface NewWorkflowProps {}
  * workflow for users to start with.
  */
 export function NewWorkflow(props: NewWorkflowProps) {
+  const dispatch = useDispatch();
   // preset workflow state
   const presetWorkflows = getPresetWorkflows();
   const [filteredWorkflows, setFilteredWorkflows] = useState<Workflow[]>(
@@ -54,12 +56,20 @@ export function NewWorkflow(props: NewWorkflowProps) {
       </EuiFlexItem>
       <EuiFlexItem>
         <EuiFlexGrid columns={3} gutterSize="l">
-          {filteredWorkflows.map((workflow: Workflow) => {
+          {filteredWorkflows.map((workflow: Workflow, index) => {
             return (
-              <EuiFlexItem>
+              <EuiFlexItem key={index}>
                 <UseCase
                   title={workflow.name}
                   description={workflow.description || ''}
+                  onClick={() =>
+                    dispatch(
+                      cacheWorkflow({
+                        ...workflow,
+                        name: toSnakeCase(workflow.name),
+                      })
+                    )
+                  }
                 />
               </EuiFlexItem>
             );
@@ -81,3 +91,14 @@ function fetchFilteredWorkflows(
         workflow.name.toLowerCase().includes(searchQuery.toLowerCase())
       );
 }
+
+// Utility fn to convert to snakecase. Used when caching the workflow
+// to make a valid name and cause less friction if users decide
+// to save it later on.
+function toSnakeCase(text: string): string {
+  return text
+    .replace(/\W+/g, ' ')
+    .split(/ |\B(?=[A-Z])/)
+    .map((word) => word.toLowerCase())
+    .join('_');
+}
diff --git a/public/pages/workflows/new_workflow/use_case.tsx b/public/pages/workflows/new_workflow/use_case.tsx
index baacb201..111c4f7c 100644
--- a/public/pages/workflows/new_workflow/use_case.tsx
+++ b/public/pages/workflows/new_workflow/use_case.tsx
@@ -13,9 +13,12 @@ import {
   EuiHorizontalRule,
   EuiButton,
 } from '@elastic/eui';
+import { NEW_WORKFLOW_ID_URL, PLUGIN_ID } from '../../../../common';
+
 interface UseCaseProps {
   title: string;
   description: string;
+  onClick: () => {};
 }
 
 export function UseCase(props: UseCaseProps) {
@@ -40,9 +43,8 @@ export function UseCase(props: UseCaseProps) {
             <EuiButton
               disabled={false}
               isLoading={false}
-              onClick={() => {
-                // TODO: possibly link to the workflow details with a pre-configured flow
-              }}
+              onClick={props.onClick}
+              href={`${PLUGIN_ID}#/workflows/${NEW_WORKFLOW_ID_URL}`}
             >
               Go
             </EuiButton>
diff --git a/public/store/reducers/workflows_reducer.ts b/public/store/reducers/workflows_reducer.ts
index 93f22e89..d06eac97 100644
--- a/public/store/reducers/workflows_reducer.ts
+++ b/public/store/reducers/workflows_reducer.ts
@@ -78,6 +78,7 @@ const initialState = {
   loading: false,
   errorMessage: '',
   workflows: {} as WorkflowDict,
+  cachedWorkflow: undefined as Workflow | undefined,
 };
 
 const WORKFLOWS_ACTION_PREFIX = 'workflows';
@@ -86,6 +87,8 @@ const SEARCH_WORKFLOWS_ACTION = `${WORKFLOWS_ACTION_PREFIX}/searchWorkflows`;
 const GET_WORKFLOW_STATE_ACTION = `${WORKFLOWS_ACTION_PREFIX}/getWorkflowState`;
 const CREATE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/createWorkflow`;
 const DELETE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/deleteWorkflow`;
+const CACHE_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/cacheWorkflow`;
+const CLEAR_CACHED_WORKFLOW_ACTION = `${WORKFLOWS_ACTION_PREFIX}/clearCachedWorkflow`;
 
 export const getWorkflow = createAsyncThunk(
   GET_WORKFLOW_ACTION,
@@ -167,6 +170,20 @@ export const deleteWorkflow = createAsyncThunk(
   }
 );
 
+export const cacheWorkflow = createAsyncThunk(
+  CACHE_WORKFLOW_ACTION,
+  async (workflow: Workflow) => {
+    return workflow;
+  }
+);
+
+// A no-op function to trigger a reducer case.
+// Will clear any stored workflow in the cachedWorkflow state
+export const clearCachedWorkflow = createAsyncThunk(
+  CLEAR_CACHED_WORKFLOW_ACTION,
+  async () => {}
+);
+
 const workflowsSlice = createSlice({
   name: 'workflows',
   initialState,
@@ -238,6 +255,13 @@ const workflowsSlice = createSlice({
         state.loading = false;
         state.errorMessage = '';
       })
+      .addCase(cacheWorkflow.fulfilled, (state, action) => {
+        const workflow = action.payload;
+        state.cachedWorkflow = workflow;
+      })
+      .addCase(clearCachedWorkflow.fulfilled, (state, action) => {
+        state.cachedWorkflow = undefined;
+      })
       // Rejected states: set state consistently across all actions
       .addCase(getWorkflow.rejected, (state, action) => {
         state.errorMessage = action.payload as string;

From 673cc8ec3c4425b95e7023281831abc51539225e Mon Sep 17 00:00:00 2001
From: Tyler Ohlsen <ohltyler@amazon.com>
Date: Fri, 1 Mar 2024 12:33:42 -0800
Subject: [PATCH 3/4] Add editor warning; add workflow fetching on cold reload

Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
---
 common/constants.ts                           |  2 +
 .../workflow_detail/components/header.tsx     | 20 +++++++---
 .../pages/workflow_detail/workflow_detail.tsx | 37 ++++++++++++++-----
 .../workflows/new_workflow/new_workflow.tsx   | 20 +++++++---
 .../pages/workflows/new_workflow/presets.tsx  |  8 +++-
 5 files changed, 65 insertions(+), 22 deletions(-)

diff --git a/common/constants.ts b/common/constants.ts
index fa890192..77d1cf52 100644
--- a/common/constants.ts
+++ b/common/constants.ts
@@ -34,3 +34,5 @@ export const DELETE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/del
  * MISCELLANEOUS
  */
 export const NEW_WORKFLOW_ID_URL = 'new';
+export const START_FROM_SCRATCH_WORKFLOW_NAME = 'Start From Scratch';
+export const DEFAULT_NEW_WORKFLOW_NAME = 'new_workflow';
diff --git a/public/pages/workflow_detail/components/header.tsx b/public/pages/workflow_detail/components/header.tsx
index 3c165ee9..2b99e57e 100644
--- a/public/pages/workflow_detail/components/header.tsx
+++ b/public/pages/workflow_detail/components/header.tsx
@@ -5,14 +5,14 @@
 
 import React, { useContext } from 'react';
 import { useDispatch, useSelector } from 'react-redux';
-import { EuiPageHeader, EuiButton } from '@elastic/eui';
-import { Workflow } from '../../../../common';
+import { EuiPageHeader, EuiButton, EuiLoadingSpinner } from '@elastic/eui';
+import { DEFAULT_NEW_WORKFLOW_NAME, Workflow } from '../../../../common';
 import { saveWorkflow } from '../utils';
 import { rfContext, AppState, removeDirty } from '../../../store';
 
 interface WorkflowDetailHeaderProps {
   tabs: any[];
-  formattedWorkflowName: string;
+  isNewWorkflow: boolean;
   workflow?: Workflow;
 }
 
@@ -23,14 +23,24 @@ export function WorkflowDetailHeader(props: WorkflowDetailHeaderProps) {
 
   return (
     <EuiPageHeader
-      pageTitle={props.formattedWorkflowName}
+      pageTitle={
+        props.workflow ? (
+          props.workflow.name
+        ) : props.isNewWorkflow && !props.workflow ? (
+          DEFAULT_NEW_WORKFLOW_NAME
+        ) : (
+          <EuiLoadingSpinner size="xl" />
+        )
+      }
       rightSideItems={[
+        // TODO: add launch logic
         <EuiButton fill={false} onClick={() => {}}>
-          Prototype
+          Launch
         </EuiButton>,
         <EuiButton
           fill={false}
           disabled={!props.workflow || !isDirty}
+          // TODO: if isNewWorkflow is true, clear the workflow cache if saving is successful.
           onClick={() => {
             // @ts-ignore
             saveWorkflow(props.workflow, reactFlowInstance);
diff --git a/public/pages/workflow_detail/workflow_detail.tsx b/public/pages/workflow_detail/workflow_detail.tsx
index f7988d77..53f23372 100644
--- a/public/pages/workflow_detail/workflow_detail.tsx
+++ b/public/pages/workflow_detail/workflow_detail.tsx
@@ -12,11 +12,14 @@ import { EuiPage, EuiPageBody } from '@elastic/eui';
 import { BREADCRUMBS } from '../../utils';
 import { getCore } from '../../services';
 import { WorkflowDetailHeader } from './components';
-import { AppState } from '../../store';
+import { AppState, searchWorkflows } from '../../store';
 import { ResizableWorkspace } from './workspace';
 import { Launches } from './launches';
 import { Prototype } from './prototype';
-import { NEW_WORKFLOW_ID_URL } from '../../../common';
+import {
+  DEFAULT_NEW_WORKFLOW_NAME,
+  NEW_WORKFLOW_ID_URL,
+} from '../../../common';
 
 export interface WorkflowDetailRouterProps {
   workflowId: string;
@@ -49,20 +52,21 @@ function replaceActiveTab(activeTab: string, props: WorkflowDetailProps) {
  * New, unsaved workflows are cached in the redux store and displayed here.
  */
 
-// TODO: if exiting the page, or if saving, clear the cached workflow. Can use redux clearCachedWorkflow()
 export function WorkflowDetail(props: WorkflowDetailProps) {
+  const dispatch = useDispatch();
   const { workflows, cachedWorkflow } = useSelector(
     (state: AppState) => state.workflows
   );
+  const { isDirty } = useSelector((state: AppState) => state.workspace);
 
-  const isNewWorkflow = props.match?.params?.workflowId === NEW_WORKFLOW_ID_URL;
-  const workflow = isNewWorkflow
-    ? cachedWorkflow
-    : workflows[props.match?.params?.workflowId];
+  // selected workflow state
+  const workflowId = props.match?.params?.workflowId;
+  const isNewWorkflow = workflowId === NEW_WORKFLOW_ID_URL;
+  const workflow = isNewWorkflow ? cachedWorkflow : workflows[workflowId];
   const workflowName = workflow
     ? workflow.name
-    : isNewWorkflow && !cachedWorkflow
-    ? 'new_workflow'
+    : isNewWorkflow && !workflow
+    ? DEFAULT_NEW_WORKFLOW_NAME
     : '';
 
   // tab state
@@ -92,6 +96,19 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
     ]);
   });
 
+  // On initial load:
+  // - fetch workflow, if there is an existing workflow ID
+  // - add a window listener to warn users if they exit/refresh
+  //   without saving latest changes
+  useEffect(() => {
+    if (!isNewWorkflow) {
+      // TODO: can optimize to only fetch a single workflow
+      dispatch(searchWorkflows({ query: { match_all: {} } }));
+    }
+    window.onbeforeunload = (e) =>
+      isDirty || isNewWorkflow ? true : undefined;
+  }, []);
+
   const tabs = [
     {
       id: WORKFLOW_DETAILS_TAB.EDITOR,
@@ -128,7 +145,7 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
         <EuiPageBody>
           <WorkflowDetailHeader
             workflow={workflow}
-            formattedWorkflowName={workflowName}
+            isNewWorkflow={isNewWorkflow}
             tabs={tabs}
           />
           {selectedTabId === WORKFLOW_DETAILS_TAB.EDITOR && (
diff --git a/public/pages/workflows/new_workflow/new_workflow.tsx b/public/pages/workflows/new_workflow/new_workflow.tsx
index 32215dd8..c255fd50 100644
--- a/public/pages/workflows/new_workflow/new_workflow.tsx
+++ b/public/pages/workflows/new_workflow/new_workflow.tsx
@@ -14,7 +14,11 @@ import {
 import { useDispatch } from 'react-redux';
 import { UseCase } from './use_case';
 import { getPresetWorkflows } from './presets';
-import { Workflow } from '../../../../common';
+import {
+  DEFAULT_NEW_WORKFLOW_NAME,
+  START_FROM_SCRATCH_WORKFLOW_NAME,
+  Workflow,
+} from '../../../../common';
 import { cacheWorkflow } from '../../../store';
 
 interface NewWorkflowProps {}
@@ -66,7 +70,7 @@ export function NewWorkflow(props: NewWorkflowProps) {
                     dispatch(
                       cacheWorkflow({
                         ...workflow,
-                        name: toSnakeCase(workflow.name),
+                        name: processWorkflowName(workflow.name),
                       })
                     )
                   }
@@ -92,9 +96,15 @@ function fetchFilteredWorkflows(
       );
 }
 
-// Utility fn to convert to snakecase. Used when caching the workflow
-// to make a valid name and cause less friction if users decide
-// to save it later on.
+// Utility fn to process workflow names from their presentable/readable titles
+// on the UI, to a valid name format.
+// This leads to less friction if users decide to save the name later on.
+function processWorkflowName(workflowName: string): string {
+  return workflowName === START_FROM_SCRATCH_WORKFLOW_NAME
+    ? DEFAULT_NEW_WORKFLOW_NAME
+    : toSnakeCase(workflowName);
+}
+
 function toSnakeCase(text: string): string {
   return text
     .replace(/\W+/g, ' ')
diff --git a/public/pages/workflows/new_workflow/presets.tsx b/public/pages/workflows/new_workflow/presets.tsx
index 2950a005..0cf7d382 100644
--- a/public/pages/workflows/new_workflow/presets.tsx
+++ b/public/pages/workflows/new_workflow/presets.tsx
@@ -3,7 +3,11 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-import { Workflow, WorkspaceFlowState } from '../../../../common';
+import {
+  START_FROM_SCRATCH_WORKFLOW_NAME,
+  Workflow,
+  WorkspaceFlowState,
+} from '../../../../common';
 
 // TODO: fetch from the backend when the workflow library is complete.
 /**
@@ -34,7 +38,7 @@ export function getPresetWorkflows(): Workflow[] {
       } as WorkspaceFlowState,
     },
     {
-      name: 'Start From Scratch',
+      name: START_FROM_SCRATCH_WORKFLOW_NAME,
       description:
         'Build your workflow from scratch according to your specific use cases. Start by adding components for your ingest or query needs.',
       useCase: '',

From c6373765051440612f09e26edc299ba2eab9665a Mon Sep 17 00:00:00 2001
From: Tyler Ohlsen <ohltyler@amazon.com>
Date: Mon, 4 Mar 2024 10:44:53 -0800
Subject: [PATCH 4/4] Skip CI if rapid is labeled

Signed-off-by: Tyler Ohlsen <ohltyler@amazon.com>
---
 .github/workflows/build-and-test.yml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 676df60f..dc4b8230 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -16,6 +16,7 @@ jobs:
       product: opensearch-dashboards
 
   build-and-test-linux:
+    if: ${{ github.event.label.name != 'rapid' }}
     needs: Get-CI-Image-Tag
     name: Build & test
     strategy:
@@ -49,7 +50,8 @@ jobs:
 
   # TODO: once github actions supports windows and macos docker containers, we can
   # merge these in to the above step's matrix, including adding windows support
-  build-and-test-windows-macos:
+  build-and-test-macos:
+    if: ${{ github.event.label.name != 'rapid' }}
     name: Build & test
     strategy:
       matrix: