diff --git a/frontend/src/components/project/ProjectDetails.tsx b/frontend/src/components/project/ProjectDetails.tsx index a5754fb8b..4c1fc1e7a 100644 --- a/frontend/src/components/project/ProjectDetails.tsx +++ b/frontend/src/components/project/ProjectDetails.tsx @@ -87,6 +87,15 @@ const DEFAULT_TABS: Record = { }, }; +function getProjectKey(project: ProjectDefinition, routeName?: string) { + return JSON.stringify({ + routeName: routeName ?? project.id, + id: project.id, + clusters: [...project.clusters].sort(), + namespaces: [...project.namespaces].sort(), + }); +} + export default function ProjectDetails() { const { t } = useTranslation(); const { name } = useParams(); @@ -95,8 +104,9 @@ export default function ProjectDetails() { if (isProjectLoading || !project || !name) { return ; } - // Key is provided to make sure we remount this component - return ; + // Remount when the route or resolved project identity changes. + const projectKey = getProjectKey(project, name); + return ; } function ProjectOverview({ @@ -408,7 +418,13 @@ const ProjectDetailsContext = createContext< /** * Project Details page */ -function ProjectDetailsContent({ project }: { project: ProjectDefinition }) { +function ProjectDetailsContent({ + project, + projectKey, +}: { + project: ProjectDefinition; + projectKey: string; +}) { const { t } = useTranslation(); const registeredTabs = useTypedSelector(state => state.projects.detailsTabs); const customDeleteButton = useTypedSelector(state => state.projects.projectDeleteButton); @@ -494,9 +510,17 @@ function ProjectDetailsContent({ project }: { project: ProjectDefinition }) { const { items, isLoading } = useProjectItems(project); - const [allTabs, setAllTabs] = useState>(DEFAULT_TABS); + const [customTabs, setCustomTabs] = useState<{ + projectKey: string; + tabs: Record; + }>({ projectKey, tabs: {} }); useEffect(() => { + let isCurrent = true; + + // Clear project-specific plugin tabs while their eligibility reloads. + setCustomTabs({ projectKey, tabs: {} }); + async function loadTabs() { const registeredTabsList = Object.values(registeredTabs); // Get a list of enabled Tabs @@ -519,28 +543,39 @@ function ProjectDetailsContent({ project }: { project: ProjectDefinition }) { ) ).filter(Boolean) as ProjectDetailsTab[]; - const enabledTabsById = Object.fromEntries(enabledTabs.map(tab => [tab.id, tab])); - - // Merge default tabs with custom tabs - const allTabs: Record = { - ...DEFAULT_TABS, - ...enabledTabsById, - }; + if (!isCurrent) return; - setAllTabs(allTabs); + const enabledTabsById = Object.fromEntries(enabledTabs.map(tab => [tab.id, tab])); + setCustomTabs({ projectKey, tabs: enabledTabsById }); } loadTabs(); - }, [registeredTabs, project]); + + return () => { + isCurrent = false; + }; + }, [registeredTabs, project, projectKey]); + + const allTabs = useMemo( + () => ({ + ...DEFAULT_TABS, + ...(customTabs.projectKey === projectKey ? customTabs.tabs : {}), + }), + [customTabs, projectKey] + ); // Set initial selected tab to the first available tab - const tabIds = Object.keys(allTabs); - if (tabIds.length > 0 && !selectedTab) { - setSelectedTab(tabIds[0]); - } + const firstTabId = Object.keys(allTabs)[0]; + + useEffect(() => { + if (firstTabId && (!selectedTab || !allTabs[selectedTab])) { + setSelectedTab(firstTabId); + } + }, [allTabs, firstTabId, selectedTab]); // Get the definition for the currently selected tab - const selectedTabData = selectedTab ? allTabs[selectedTab] : undefined; + const activeTab = selectedTab && allTabs[selectedTab] ? selectedTab : firstTabId; + const selectedTabData = activeTab ? allTabs[activeTab] : undefined; const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { setSelectedTab(newValue); @@ -593,7 +628,7 @@ function ProjectDetailsContent({ project }: { project: ProjectDefinition }) { >