Skip to content

Commit

Permalink
Keep project selection per page
Browse files Browse the repository at this point in the history
  • Loading branch information
jeff-phillips-18 committed Feb 3, 2025
1 parent 63f2839 commit fe39c46
Show file tree
Hide file tree
Showing 30 changed files with 147 additions and 56 deletions.
23 changes: 22 additions & 1 deletion frontend/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,20 @@ import './App.scss';

type PocConfigType = {
altNav?: boolean;
altPreferredProject?: boolean;
};

const FAVORITE_PROJECTS_KEY = 'odh-favorite-projects';
const POC_SESSION_KEY = 'odh-poc-flags';
const ALT_NAV_PARAM = 'altNav';
const ALT_PROJECTS_PARAM = 'altProjects';

const App: React.FC = () => {
const [notificationsOpen, setNotificationsOpen] = React.useState(false);
const { username, userError, isAllowed } = useUser();
const [searchParams, setSearchParams] = useSearchParams();
const [altNav, setAltNav] = React.useState<boolean>(false);
const [altPreferredProject, setAltPreferredProject] = React.useState<boolean>(false);
const firstLoad = React.useRef(true);
const [pocConfig, setPocConfig] = useBrowserStorage<PocConfigType | null>(
POC_SESSION_KEY,
Expand Down Expand Up @@ -96,6 +99,15 @@ const App: React.FC = () => {
searchParams.delete(ALT_NAV_PARAM);
setSearchParams(searchParams, { replace: true });
}
if (searchParams.has(ALT_PROJECTS_PARAM)) {
const updated = searchParams.get(ALT_PROJECTS_PARAM) === 'true';
setAltPreferredProject(updated);
setPocConfig({ altNav: updated });

// clean up query string
searchParams.delete(ALT_NAV_PARAM);
setSearchParams(searchParams, { replace: true });
}
// do not react to changes to setters
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams]);
Expand All @@ -109,11 +121,20 @@ const App: React.FC = () => {
storageClasses,
isRHOAI: dashboardConfig.metadata?.namespace === 'redhat-ods-applications',
altNav,
altPreferredProject,
favoriteProjects,
setFavoriteProjects,
}
: null,
[dashboardConfig, buildStatuses, storageClasses, altNav, favoriteProjects, setFavoriteProjects],
[
dashboardConfig,
buildStatuses,
storageClasses,
altNav,
altPreferredProject,
favoriteProjects,
setFavoriteProjects,
],
);

const isUnauthorized = fetchConfigError?.request?.status === 403;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/AppContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type AppContextProps = {
storageClasses: StorageClassKind[];
isRHOAI: boolean;
altNav?: boolean;
altPreferredProject?: boolean;
favoriteProjects: string[];
setFavoriteProjects: (projects: string[]) => void;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const DistributedWorkloadsContextProvider =
)(({ children, namespace }) => {
const { projects } = React.useContext(ProjectsContext);
const project = projects.find(byName(namespace)) ?? null;
useSyncPreferredProject(project);
useSyncPreferredProject('distributed-workloads', project);

const { currentRefreshInterval } = React.useContext(MetricsCommonContext);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const PipelineContextProvider = conditionalArea<PipelineContextProviderPr
)(({ children, namespace }) => {
const { projects } = React.useContext(ProjectsContext);
const project = projects.find(byName(namespace)) ?? null;
useSyncPreferredProject(project);
useSyncPreferredProject('pipelines', project);

const state = usePipelineNamespaceCR(namespace);
const [pipelineNamespaceCR, crLoaded, crLoadError, refreshCR] = state;
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/concepts/projects/InvalidProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@ import EmptyStateErrorMessage from '~/components/EmptyStateErrorMessage';
import ProjectSelectorNavigator from '~/concepts/projects/ProjectSelectorNavigator';

type InvalidProjectProps = {
page: string;
title?: string;
namespace?: string;
getRedirectPath: (namespace: string) => string;
};

const InvalidProject: React.FC<InvalidProjectProps> = ({ namespace, title, getRedirectPath }) => (
const InvalidProject: React.FC<InvalidProjectProps> = ({
page,
namespace,
title,
getRedirectPath,
}) => (
<EmptyStateErrorMessage
title={title || 'Project not found'}
bodyText={`${namespace ? `Project ${namespace}` : 'The Project'} was not found.`}
>
<ProjectSelectorNavigator
page={page}
getRedirectPath={getRedirectPath}
invalidDropdownPlaceholder="Select project"
primary
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/concepts/projects/ProjectSelectorNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { ProjectsContext } from '~/concepts/projects/ProjectsContext';
import ProjectSelector from './ProjectSelector';

type ProjectSelectorProps = {
page: string;
getRedirectPath: (namespace: string) => string;
} & Omit<React.ComponentProps<typeof ProjectSelector>, 'onSelection' | 'namespace'>;

const ProjectSelectorNavigator: React.FC<ProjectSelectorProps> = ({
page,
getRedirectPath,
...projectSelectorProps
}) => {
Expand All @@ -19,9 +21,7 @@ const ProjectSelectorNavigator: React.FC<ProjectSelectorProps> = ({
<ProjectSelector
{...projectSelectorProps}
onSelection={(projectName) => {
if (!projectName) {
updatePreferredProject(null);
}
updatePreferredProject(page, projectName || null);
navigate(getRedirectPath(projectName));
}}
namespace={namespace ?? ''}
Expand Down
40 changes: 29 additions & 11 deletions frontend/src/concepts/projects/ProjectsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FetchState } from '~/utilities/useFetchState';
import { KnownLabels, ProjectKind } from '~/k8sTypes';
import { useDashboardNamespace } from '~/redux/selectors';
import { getDisplayNameFromK8sResource } from '~/concepts/k8s/utils';
import { AppContext } from '~/app/AppContext';
import { isAvailableProject } from './utils';

const projectSorter = (projectA: ProjectKind, projectB: ProjectKind) =>
Expand All @@ -16,13 +17,13 @@ type ProjectsContextType = {
/** eg. Terminating state, etc */
nonActiveProjects: ProjectKind[];

/** Some component set this value, you should use this instead of projects[0] */
preferredProject: ProjectKind | null;
/** Get the project last used for the given page] */
getPreferredProject: (page: string) => ProjectKind | null;
/**
* Allows for navigation to be unimpeded by project selection
* @see useSyncPreferredProject
*/
updatePreferredProject: (project: ProjectKind | null) => void;
updatePreferredProject: (page: string, projectName: string | null) => void;
waitForProject: (projectName: string) => Promise<void>;

// ...the rest of the state variables
Expand All @@ -34,8 +35,9 @@ export const ProjectsContext = React.createContext<ProjectsContextType>({
projects: [],
modelServingProjects: [],
nonActiveProjects: [],
preferredProject: null,
updatePreferredProject: () => undefined,
getPreferredProject: () => null,
// eslint-disable-next-line @typescript-eslint/no-empty-function
updatePreferredProject: () => {},
loaded: false,
loadError: new Error('Not in project provider'),
waitForProject: () => Promise.resolve(),
Expand All @@ -50,10 +52,26 @@ type ProjectsProviderProps = {
};

const ProjectsContextProvider: React.FC<ProjectsProviderProps> = ({ children }) => {
const [preferredProject, setPreferredProject] =
React.useState<ProjectsContextType['preferredProject']>(null);
const [projectData, loaded, loadError] = useProjects();
const { dashboardNamespace } = useDashboardNamespace();
const { altPreferredProject } = React.useContext(AppContext);
const [pageProjects, setPageProjects] = React.useState<{ [key: string]: ProjectKind | null }>({});

const getPreferredProject = React.useCallback(
(page: string) => pageProjects[!altPreferredProject ? page : '__preferredProject'] ?? null,
[altPreferredProject, pageProjects],
);
const updatePreferredProject = React.useCallback(
(page: string, projectName: string | null) => {
const project = projectData.find((p) => p.metadata.name === projectName);
setPageProjects((prev) => {
const projects = { ...prev };
projects[!altPreferredProject ? page : '__preferredProject'] = project || null;
return projects;
});
},
[altPreferredProject, projectData],
);

const { projects, modelServingProjects, nonActiveProjects } = React.useMemo(
() =>
Expand Down Expand Up @@ -121,8 +139,8 @@ const ProjectsContextProvider: React.FC<ProjectsProviderProps> = ({ children })
projects: projects.toSorted(projectSorter),
modelServingProjects: modelServingProjects.toSorted(projectSorter),
nonActiveProjects: nonActiveProjects.toSorted(projectSorter),
preferredProject,
updatePreferredProject: setPreferredProject,
getPreferredProject,
updatePreferredProject,
loaded,
loadError,
waitForProject,
Expand All @@ -131,8 +149,8 @@ const ProjectsContextProvider: React.FC<ProjectsProviderProps> = ({ children })
projects,
modelServingProjects,
nonActiveProjects,
preferredProject,
setPreferredProject,
getPreferredProject,
updatePreferredProject,
loaded,
loadError,
waitForProject,
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/concepts/projects/useSyncPreferredProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import * as React from 'react';
import { ProjectsContext } from '~/concepts/projects/ProjectsContext';
import { ProjectKind } from '~/k8sTypes';

const useSyncPreferredProject = (newPreferredProject: ProjectKind | null): void => {
const { preferredProject, updatePreferredProject } = React.useContext(ProjectsContext);
const useSyncPreferredProject = (page: string, newPreferredProject: ProjectKind | null): void => {
const { getPreferredProject, updatePreferredProject } = React.useContext(ProjectsContext);
const preferredProject = getPreferredProject(page);

React.useEffect(() => {
if (newPreferredProject?.metadata.name !== preferredProject?.metadata.name) {
updatePreferredProject(newPreferredProject);
updatePreferredProject(page, newPreferredProject?.metadata.name || null);
}
}, [newPreferredProject, preferredProject, updatePreferredProject]);
}, [page, newPreferredProject, preferredProject, updatePreferredProject]);
};

export default useSyncPreferredProject;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { ProjectsContext } from '~/concepts/projects/ProjectsContext';
import StorageList from '~/pages/projects/screens/detail/storage/StorageList';

const GlobalClusterStoragePage: React.FC = () => {
const { projects, preferredProject, updatePreferredProject } = React.useContext(ProjectsContext);
const { getPreferredProject, updatePreferredProject } = React.useContext(ProjectsContext);
const preferredProject = getPreferredProject('cluster-storage');
const navigate = useNavigate();
return (
<PageSection hasBodyWrapper={false}>
Expand All @@ -17,8 +18,7 @@ const GlobalClusterStoragePage: React.FC = () => {
selectAllProjects
invalidDropdownPlaceholder="All projects"
onSelection={(projectName) => {
const project = projects.find((p) => p.metadata.name === projectName);
updatePreferredProject(project || null);
updatePreferredProject('cluster-storage', projectName);
navigate('/clusterStorage');
}}
namespace={preferredProject?.metadata.name ?? 'all-projects'}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/pages/connections/GlobalConnectionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { ProjectsContext } from '~/concepts/projects/ProjectsContext';
import ConnectionsList from '~/pages/projects/screens/detail/connections/ConnectionsList';

const GlobalConnectionsPage: React.FC = () => {
const { projects, preferredProject, updatePreferredProject } = React.useContext(ProjectsContext);
const { getPreferredProject, updatePreferredProject } = React.useContext(ProjectsContext);
const preferredProject = getPreferredProject('connections');
const navigate = useNavigate();
return (
<PageSection hasBodyWrapper={false}>
Expand All @@ -17,8 +18,7 @@ const GlobalConnectionsPage: React.FC = () => {
selectAllProjects
invalidDropdownPlaceholder="All projects"
onSelection={(projectName) => {
const project = projects.find((p) => p.metadata.name === projectName);
updatePreferredProject(project || null);
updatePreferredProject('connections', projectName);
navigate('/connections');
}}
namespace={preferredProject?.metadata.name ?? 'all-projects'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const GlobalDistributedWorkloads: React.FC<GlobalDistributedWorkloadsProps> = ({
getInvalidRedirectPath,
}) => {
const { namespace } = useParams<{ namespace: string }>();
const { projects, preferredProject } = React.useContext(ProjectsContext);
const { projects, getPreferredProject } = React.useContext(ProjectsContext);
const preferredProject = getPreferredProject('distributed-workloads');

if (projects.length === 0) {
return (
Expand All @@ -52,7 +53,11 @@ const GlobalDistributedWorkloads: React.FC<GlobalDistributedWorkloadsProps> = ({
loaded
empty
emptyStatePage={
<InvalidProject namespace={namespace} getRedirectPath={getInvalidRedirectPath} />
<InvalidProject
page="distributed-workloads"
namespace={namespace}
getRedirectPath={getInvalidRedirectPath}
/>
}
/>
);
Expand All @@ -67,6 +72,7 @@ const GlobalDistributedWorkloads: React.FC<GlobalDistributedWorkloadsProps> = ({
empty={false}
headerContent={
<ProjectSelectorNavigator
page="distributed-workloads"
getRedirectPath={(ns: string) => `/distributedWorkloads/${activeTab.path}/${ns}`}
showTitle
/>
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/pages/modelServing/ModelServingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ const ModelServingContextProvider = conditionalArea<ModelServingContextProviderP
)(({ children, namespace, getErrorComponent }) => {
const { dashboardNamespace } = useDashboardNamespace();
const navigate = useNavigate();
const { projects, preferredProject } = React.useContext(ProjectsContext);
const { projects, getPreferredProject } = React.useContext(ProjectsContext);
const preferredProject = getPreferredProject('model-serving');
const project = projects.find(byName(namespace)) ?? null;
useSyncPreferredProject(project);
useSyncPreferredProject('model-serving', project);
const servingRuntimeTemplates = useTemplates(dashboardNamespace);

const servingRuntimeTemplateOrder = useContextResourceData<string>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const GlobalModelServingCoreLoader: React.FC<GlobalModelServingCoreLoaderProps>
getInvalidRedirectPath,
}) => {
const { namespace } = useParams<{ namespace: string }>();
const { projects, preferredProject } = React.useContext(ProjectsContext);
const { projects, getPreferredProject } = React.useContext(ProjectsContext);
const preferredProject = getPreferredProject('model-serving');

let renderStateProps: ApplicationPageRenderState & { children?: React.ReactNode };
if (projects.length === 0) {
Expand All @@ -43,7 +44,11 @@ const GlobalModelServingCoreLoader: React.FC<GlobalModelServingCoreLoaderProps>
renderStateProps = {
empty: true,
emptyStatePage: (
<InvalidProject namespace={namespace} getRedirectPath={getInvalidRedirectPath} />
<InvalidProject
page="model-serving"
namespace={namespace}
getRedirectPath={getInvalidRedirectPath}
/>
),
};
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const ModelServingProjectSelection: React.FC<ModelServingProjectSelectionProps>
getRedirectPath,
}) => (
<ProjectSelectorNavigator
page="model-serving"
getRedirectPath={getRedirectPath}
invalidDropdownPlaceholder="All projects"
selectAllProjects
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/pages/notebooks/GlobalNotebooksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { ProjectsContext } from '~/concepts/projects/ProjectsContext';
import NotebookList from '~/pages/projects/screens/detail/notebooks/NotebookList';

const GlobalNotebooksPage: React.FC = () => {
const { projects, preferredProject, updatePreferredProject } = React.useContext(ProjectsContext);
const { getPreferredProject, updatePreferredProject } = React.useContext(ProjectsContext);
const preferredProject = getPreferredProject('notebooks');
const navigate = useNavigate();
return (
<PageSection hasBodyWrapper={false}>
Expand All @@ -18,8 +19,7 @@ const GlobalNotebooksPage: React.FC = () => {
selectAllProjects
invalidDropdownPlaceholder="All projects"
onSelection={(projectName) => {
const project = projects.find((p) => p.metadata.name === projectName);
updatePreferredProject(project || null);
updatePreferredProject('notebooks', projectName);
navigate('/workbenches');
}}
namespace={preferredProject?.metadata.name ?? 'all-projects'}
Expand Down
Loading

0 comments on commit fe39c46

Please sign in to comment.