diff --git a/backend/src/routes/api/namespaces/namespaceUtils.ts b/backend/src/routes/api/namespaces/namespaceUtils.ts index 05b1a7dc7f..c55387debb 100644 --- a/backend/src/routes/api/namespaces/namespaceUtils.ts +++ b/backend/src/routes/api/namespaces/namespaceUtils.ts @@ -65,7 +65,6 @@ export const applyNamespaceChange = async ( case NamespaceApplicationCase.DSG_CREATION: labels = { 'opendatahub.io/dashboard': 'true', - 'modelmesh-enabled': 'true', }; break; case NamespaceApplicationCase.MODEL_SERVING_PROMOTION: diff --git a/frontend/src/api/k8s/projects.ts b/frontend/src/api/k8s/projects.ts index 02224aa101..6426c79578 100644 --- a/frontend/src/api/k8s/projects.ts +++ b/frontend/src/api/k8s/projects.ts @@ -27,9 +27,6 @@ export const getProjects = (withLabel?: string): Promise => queryOptions: withLabel ? { queryParams: { labelSelector: withLabel } } : undefined, }).then((listResource) => listResource.items); -export const getDSGProjects = (): Promise => - getProjects(LABEL_SELECTOR_DASHBOARD_RESOURCE); - export const createProject = ( username: string, displayName: string, diff --git a/frontend/src/concepts/projects/ProjectsContext.tsx b/frontend/src/concepts/projects/ProjectsContext.tsx index b9e25bca1e..7d3342965d 100644 --- a/frontend/src/concepts/projects/ProjectsContext.tsx +++ b/frontend/src/concepts/projects/ProjectsContext.tsx @@ -1,11 +1,13 @@ import * as React from 'react'; import useFetchState, { FetchState } from '~/utilities/useFetchState'; -import { getDSGProjects } from '~/api'; +import { getProjects } from '~/api'; import { KnownLabels, ProjectKind } from '~/k8sTypes'; +import { useDashboardNamespace } from '~/redux/selectors'; type ProjectFetchState = FetchState; type ProjectsContext = { projects: ProjectKind[]; + dataScienceProjects: ProjectKind[]; modelServingProjects: ProjectKind[]; /** eg. Terminating state, etc */ nonActiveProjects: ProjectKind[]; @@ -26,6 +28,7 @@ type ProjectsContext = { export const ProjectsContext = React.createContext({ projects: [], + dataScienceProjects: [], modelServingProjects: [], nonActiveProjects: [], preferredProject: null, @@ -44,39 +47,55 @@ type ProjectsProviderProps = { }; const ProjectsContextProvider: React.FC = ({ children }) => { - const fetchProjects = React.useCallback(() => getDSGProjects(), []); + const fetchProjects = React.useCallback(() => getProjects(), []); const [preferredProject, setPreferredProject] = React.useState(null); const [projectData, loaded, loadError, refreshProjects] = useFetchState( fetchProjects, [], ); + const { dashboardNamespace } = useDashboardNamespace(); - const { projects, modelServingProjects, nonActiveProjects } = React.useMemo( + const { projects, dataScienceProjects, modelServingProjects, nonActiveProjects } = React.useMemo( () => projectData.reduce<{ projects: ProjectKind[]; + dataScienceProjects: ProjectKind[]; modelServingProjects: ProjectKind[]; nonActiveProjects: ProjectKind[]; }>( (states, project) => { - if (project.status?.phase === 'Active') { - // Project that is active - states.projects.push(project); - if (project.metadata.labels?.[KnownLabels.MODEL_SERVING_PROJECT]) { - // Model Serving active projects - states.modelServingProjects.push(project); + if ( + !( + project.metadata.name.startsWith('openshift-') || + project.metadata.name.startsWith('kube-') || + project.metadata.name === 'default' || + project.metadata.name === 'system' || + project.metadata.name === 'openshift' || + project.metadata.name === dashboardNamespace + ) + ) { + if (project.status?.phase === 'Active') { + // Project that is active + states.projects.push(project); + if (project.metadata.labels?.[KnownLabels.DASHBOARD_RESOURCE]) { + states.dataScienceProjects.push(project); + } + if (project.metadata.labels?.[KnownLabels.MODEL_SERVING_PROJECT]) { + // Model Serving active projects + states.modelServingProjects.push(project); + } + } else { + // Non 'Active' -- aka terminating + states.nonActiveProjects.push(project); } - } else { - // Non 'Active' -- aka terminating - states.nonActiveProjects.push(project); } return states; }, - { projects: [], modelServingProjects: [], nonActiveProjects: [] }, + { projects: [], dataScienceProjects: [], modelServingProjects: [], nonActiveProjects: [] }, ), - [projectData], + [projectData, dashboardNamespace], ); const refresh = React.useCallback( @@ -114,6 +133,7 @@ const ProjectsContextProvider: React.FC = ({ children }) = ({ ]) .then(() => Promise.all([ - ...(currentProject.metadata.labels?.['modelmesh-enabled'] && allowCreate + ...(currentProject.metadata.labels?.['modelmesh-enabled'] === undefined && allowCreate ? [addSupportModelMeshProject(currentProject.metadata.name)] : []), ...(editInfo?.servingRuntime diff --git a/frontend/src/pages/projects/screens/projects/ProjectListView.tsx b/frontend/src/pages/projects/screens/projects/ProjectListView.tsx index 757853dba4..cdd3751c6f 100644 --- a/frontend/src/pages/projects/screens/projects/ProjectListView.tsx +++ b/frontend/src/pages/projects/screens/projects/ProjectListView.tsx @@ -2,13 +2,13 @@ import * as React from 'react'; import { Button, ButtonVariant, ToolbarItem } from '@patternfly/react-core'; import { useNavigate } from 'react-router-dom'; import Table from '~/components/table/Table'; -import useTableColumnSort from '~/components/table/useTableColumnSort'; import SearchField, { SearchType } from '~/pages/projects/components/SearchField'; import { ProjectKind } from '~/k8sTypes'; import { getProjectDisplayName, getProjectOwner } from '~/pages/projects/utils'; import { useAppContext } from '~/app/AppContext'; import LaunchJupyterButton from '~/pages/projects/screens/projects/LaunchJupyterButton'; import { ProjectsContext } from '~/concepts/projects/ProjectsContext'; +import { ProjectScope } from '~/pages/projects/types'; import NewProjectButton from './NewProjectButton'; import { columns } from './tableData'; import ProjectTableRow from './ProjectTableRow'; @@ -17,16 +17,18 @@ import ManageProjectModal from './ManageProjectModal'; type ProjectListViewProps = { allowCreate: boolean; + scope: ProjectScope; }; -const ProjectListView: React.FC = ({ allowCreate }) => { +const ProjectListView: React.FC = ({ allowCreate, scope }) => { const { dashboardConfig } = useAppContext(); - const { projects: unfilteredProjects, refresh } = React.useContext(ProjectsContext); + const { projects, dataScienceProjects, refresh } = React.useContext(ProjectsContext); const navigate = useNavigate(); const [searchType, setSearchType] = React.useState(SearchType.NAME); const [search, setSearch] = React.useState(''); - const sort = useTableColumnSort(columns, 0); - const filteredProjects = sort.transformData(unfilteredProjects).filter((project) => { + const filteredProjects = ( + scope === ProjectScope.ALL_PROJECTS ? projects : dataScienceProjects + ).filter((project) => { if (!search) { return true; } diff --git a/frontend/src/pages/projects/screens/projects/ProjectScopeSelect.tsx b/frontend/src/pages/projects/screens/projects/ProjectScopeSelect.tsx new file mode 100644 index 0000000000..b5512f73db --- /dev/null +++ b/frontend/src/pages/projects/screens/projects/ProjectScopeSelect.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { Select, SelectOption } from '@patternfly/react-core'; +import { isInEnum } from '~/typeHelpers'; +import { ProjectScope } from '~/pages/projects/types'; + +type ProjectScopeSelectProps = { + selection: ProjectScope; + setSelection: (selection: ProjectScope) => void; +}; + +const isProjectScope = isInEnum(ProjectScope); + +const ProjectScopeSelect: React.FC = ({ selection, setSelection }) => { + const [isOpen, setOpen] = React.useState(false); + return ( + + ); +}; + +export default ProjectScopeSelect; diff --git a/frontend/src/pages/projects/screens/projects/ProjectTableRow.tsx b/frontend/src/pages/projects/screens/projects/ProjectTableRow.tsx index a54934e7dd..cba59d1fd9 100644 --- a/frontend/src/pages/projects/screens/projects/ProjectTableRow.tsx +++ b/frontend/src/pages/projects/screens/projects/ProjectTableRow.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { Text, TextVariants, Timestamp } from '@patternfly/react-core'; +import { Flex, Label, Text, TextVariants, Timestamp, Tooltip } from '@patternfly/react-core'; import { ActionsColumn, Td, Tr } from '@patternfly/react-table'; -import { ProjectKind } from '~/k8sTypes'; +import { KnownLabels, ProjectKind } from '~/k8sTypes'; import useProjectTableRowItems from '~/pages/projects/screens/projects/useProjectTableRowItems'; import useProjectNotebookStates from '~/pages/projects/notebook/useProjectNotebookStates'; import ListNotebookState from '~/pages/projects/notebook/ListNotebookState'; @@ -28,9 +28,18 @@ const ProjectTableRow: React.FC = ({ return ( - - - + + {project.metadata.labels?.[KnownLabels.DASHBOARD_RESOURCE] && ( + + + + )} + + + + {owner && {owner}} diff --git a/frontend/src/pages/projects/screens/projects/ProjectView.tsx b/frontend/src/pages/projects/screens/projects/ProjectView.tsx index a5417e8169..b97ea06781 100644 --- a/frontend/src/pages/projects/screens/projects/ProjectView.tsx +++ b/frontend/src/pages/projects/screens/projects/ProjectView.tsx @@ -4,8 +4,11 @@ import { useAccessReview } from '~/api'; import { AccessReviewResourceAttributes } from '~/k8sTypes'; import { ProjectsContext } from '~/concepts/projects/ProjectsContext'; import useMountProjectRefresh from '~/concepts/projects/useMountProjectRefresh'; +import { useBrowserStorage } from '~/components/browserStorage'; +import { ProjectScope } from '~/pages/projects/types'; import EmptyProjects from './EmptyProjects'; import ProjectListView from './ProjectListView'; +import ProjectScopeSelect from './ProjectScopeSelect'; const accessReviewResource: AccessReviewResourceAttributes = { group: 'project.openshift.io', @@ -14,7 +17,11 @@ const accessReviewResource: AccessReviewResourceAttributes = { }; const ProjectView: React.FC = () => { - const { projects } = React.useContext(ProjectsContext); + const [scope, setScope] = useBrowserStorage( + 'odh.dashboard.project.scope', + ProjectScope.DS_PROJECTS, + ); + const { projects, dataScienceProjects } = React.useContext(ProjectsContext); useMountProjectRefresh(); const [allowCreate, rbacLoaded] = useAccessReview(accessReviewResource); @@ -26,12 +33,17 @@ const ProjectView: React.FC = () => { ? `View your existing projects${allowCreate ? ' or create new projects' : ''}.` : undefined } + headerContent={} loaded={rbacLoaded} - empty={projects.length === 0} + empty={ + scope === ProjectScope.ALL_PROJECTS + ? projects.length === 0 + : dataScienceProjects.length === 0 + } emptyStatePage={} provideChildrenPadding > - + ); }; diff --git a/frontend/src/pages/projects/types.ts b/frontend/src/pages/projects/types.ts index 89de7934e3..74645f665d 100644 --- a/frontend/src/pages/projects/types.ts +++ b/frontend/src/pages/projects/types.ts @@ -136,3 +136,7 @@ export enum ConfigMapCategory { GENERIC = 'configmap key-value', UPLOAD = 'configmap upload', } +export enum ProjectScope { + DS_PROJECTS = 'Data science projects', + ALL_PROJECTS = 'All available projects', +} diff --git a/frontend/src/typeHelpers.ts b/frontend/src/typeHelpers.ts index 65026cdb5f..48cb3f1e57 100644 --- a/frontend/src/typeHelpers.ts +++ b/frontend/src/typeHelpers.ts @@ -93,3 +93,8 @@ export type EitherNotBoth = (TypeA & Never) | (TypeB & Neve export type EitherOrNone = | EitherNotBoth | (Never & Never); + +export const isInEnum = + (e: T) => + (token: unknown): token is T[keyof T] => + Object.values(e).includes(token as T[keyof T]);