mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
feat: command bar pages and name resolving (#7397)
This commit is contained in:
parent
25947c3075
commit
9b789ea5ef
@ -21,6 +21,8 @@ import {
|
|||||||
CommandResultGroup,
|
CommandResultGroup,
|
||||||
type CommandResultGroupItem,
|
type CommandResultGroupItem,
|
||||||
} from './RecentlyVisited/CommandResultGroup';
|
} from './RecentlyVisited/CommandResultGroup';
|
||||||
|
import { PageSuggestions } from './PageSuggestions';
|
||||||
|
import { useRoutes } from 'component/layout/MainLayout/NavigationSidebar/useRoutes';
|
||||||
import { useAsyncDebounce } from 'react-table';
|
import { useAsyncDebounce } from 'react-table';
|
||||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||||
|
|
||||||
@ -89,6 +91,23 @@ export const CommandBar = () => {
|
|||||||
CommandResultGroupItem[]
|
CommandResultGroupItem[]
|
||||||
>([]);
|
>([]);
|
||||||
const { lastVisited } = useRecentlyVisited();
|
const { lastVisited } = useRecentlyVisited();
|
||||||
|
const { routes } = useRoutes();
|
||||||
|
const allRoutes: Record<
|
||||||
|
string,
|
||||||
|
{ path: string; route: string; title: string }
|
||||||
|
> = {};
|
||||||
|
for (const route of [
|
||||||
|
...routes.mainNavRoutes,
|
||||||
|
...routes.adminRoutes,
|
||||||
|
...routes.mobileRoutes,
|
||||||
|
]) {
|
||||||
|
allRoutes[route.path] = {
|
||||||
|
path: route.path,
|
||||||
|
route: route.route,
|
||||||
|
title: route.title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const hideSuggestions = () => {
|
const hideSuggestions = () => {
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
};
|
};
|
||||||
@ -212,7 +231,11 @@ export const CommandBar = () => {
|
|||||||
elseShow={
|
elseShow={
|
||||||
showSuggestions && (
|
showSuggestions && (
|
||||||
<CommandResultsPaper className='dropdown-outline'>
|
<CommandResultsPaper className='dropdown-outline'>
|
||||||
<RecentlyVisited lastVisited={lastVisited} />
|
<RecentlyVisited
|
||||||
|
lastVisited={lastVisited}
|
||||||
|
routes={allRoutes}
|
||||||
|
/>
|
||||||
|
<PageSuggestions routes={allRoutes} />
|
||||||
</CommandResultsPaper>
|
</CommandResultsPaper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
89
frontend/src/component/commandBar/PageSuggestions.tsx
Normal file
89
frontend/src/component/commandBar/PageSuggestions.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
List,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
styled,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { IconRenderer } from 'component/layout/MainLayout/NavigationSidebar/IconRenderer';
|
||||||
|
import type { Theme } from '@mui/material/styles/createTheme';
|
||||||
|
|
||||||
|
const listItemButtonStyle = (theme: Theme) => ({
|
||||||
|
borderRadius: theme.spacing(0.5),
|
||||||
|
borderLeft: `${theme.spacing(0.5)} solid transparent`,
|
||||||
|
'&.Mui-selected': {
|
||||||
|
borderLeft: `${theme.spacing(0.5)} solid ${theme.palette.primary.main}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledTypography = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.bodySize,
|
||||||
|
padding: theme.spacing(0, 3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledListItemIcon = styled(ListItemIcon)(({ theme }) => ({
|
||||||
|
minWidth: theme.spacing(4),
|
||||||
|
margin: theme.spacing(0.25, 0),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledListItemText = styled(ListItemText)(({ theme }) => ({
|
||||||
|
margin: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const toListItemData = (
|
||||||
|
items: string[],
|
||||||
|
routes: Record<string, { path: string; route: string; title: string }>,
|
||||||
|
) => {
|
||||||
|
return items.map((item) => {
|
||||||
|
return {
|
||||||
|
name: routes[item]?.title ?? item,
|
||||||
|
path: item,
|
||||||
|
icon: <IconRenderer path={item} />,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const pages = [
|
||||||
|
'/search',
|
||||||
|
'/integrations',
|
||||||
|
'/environments',
|
||||||
|
'/context',
|
||||||
|
'/segments',
|
||||||
|
'/tag-types',
|
||||||
|
'/applications',
|
||||||
|
'/strategies',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PageSuggestions = ({
|
||||||
|
routes,
|
||||||
|
}: {
|
||||||
|
routes: Record<string, { path: string; route: string; title: string }>;
|
||||||
|
}) => {
|
||||||
|
const filtered = pages.filter((page) => routes[page]);
|
||||||
|
const pageItems = toListItemData(filtered, routes);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledTypography color='textSecondary'>Pages</StyledTypography>
|
||||||
|
<List>
|
||||||
|
{pageItems.map((item, index) => (
|
||||||
|
<ListItemButton
|
||||||
|
key={`recently-visited-${index}`}
|
||||||
|
dense={true}
|
||||||
|
component={Link}
|
||||||
|
to={item.path}
|
||||||
|
sx={listItemButtonStyle}
|
||||||
|
>
|
||||||
|
<StyledListItemIcon>{item.icon}</StyledListItemIcon>
|
||||||
|
<StyledListItemText>
|
||||||
|
<Typography color='textPrimary'>
|
||||||
|
{item.name}
|
||||||
|
</Typography>
|
||||||
|
</StyledListItemText>
|
||||||
|
</ListItemButton>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -14,6 +14,8 @@ import {
|
|||||||
} from 'component/layout/MainLayout/NavigationSidebar/IconRenderer';
|
} from 'component/layout/MainLayout/NavigationSidebar/IconRenderer';
|
||||||
import type { LastViewedPage } from 'hooks/useRecentlyVisited';
|
import type { LastViewedPage } from 'hooks/useRecentlyVisited';
|
||||||
import type { Theme } from '@mui/material/styles/createTheme';
|
import type { Theme } from '@mui/material/styles/createTheme';
|
||||||
|
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
const listItemButtonStyle = (theme: Theme) => ({
|
const listItemButtonStyle = (theme: Theme) => ({
|
||||||
borderRadius: theme.spacing(0.5),
|
borderRadius: theme.spacing(0.5),
|
||||||
@ -37,55 +39,127 @@ const StyledListItemText = styled(ListItemText)(({ theme }) => ({
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const toListItemData = (lastVisited: LastViewedPage[]) => {
|
const toListItemButton = (
|
||||||
return lastVisited.map((item) => {
|
item: LastViewedPage,
|
||||||
if (item.featureId) {
|
routes: Record<string, { path: string; route: string; title: string }>,
|
||||||
return {
|
index: number,
|
||||||
name: item.featureId,
|
) => {
|
||||||
path: `/projects/${item.projectId}/features/${item.featureId}`,
|
const key = `recently-visited-${index}`;
|
||||||
icon: <Icon>{'flag'}</Icon>,
|
if (item.featureId && item.projectId) {
|
||||||
};
|
return (
|
||||||
|
<RecentlyVisitedFeatureButton
|
||||||
|
key={key}
|
||||||
|
featureId={item.featureId}
|
||||||
|
projectId={item.projectId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (item.projectId) {
|
if (item.projectId) {
|
||||||
return {
|
return (
|
||||||
name: item.projectId,
|
<RecentlyVisitedProjectButton
|
||||||
path: `/projects/${item.projectId}`,
|
key={key}
|
||||||
icon: <StyledProjectIcon />,
|
projectId={item.projectId}
|
||||||
};
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return {
|
if (!item.pathName) return null;
|
||||||
name: item.featureId ?? item.projectId ?? item.pathName,
|
const name = routes[item.pathName]?.title ?? item.pathName;
|
||||||
path: item.pathName || '/',
|
return (
|
||||||
icon: <IconRenderer path={item.pathName ?? '/unknown'} />,
|
<RecentlyVisitedPathButton key={key} path={item.pathName} name={name} />
|
||||||
};
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const RecentlyVisitedFeatureButton = ({
|
||||||
|
key,
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
}: { key: string; projectId: string; featureId: string }) => {
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
key={key}
|
||||||
|
dense={true}
|
||||||
|
component={Link}
|
||||||
|
to={`/projects/${projectId}/features/${featureId}`}
|
||||||
|
sx={listItemButtonStyle}
|
||||||
|
>
|
||||||
|
<StyledListItemIcon>
|
||||||
|
<Icon>{'flag'}</Icon>
|
||||||
|
</StyledListItemIcon>
|
||||||
|
<StyledListItemText>
|
||||||
|
<Typography color='textPrimary'>{featureId}</Typography>
|
||||||
|
</StyledListItemText>
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecentlyVisitedPathButton = ({
|
||||||
|
path,
|
||||||
|
key,
|
||||||
|
name,
|
||||||
|
}: { path: string; key: string; name: string }) => {
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
key={key}
|
||||||
|
dense={true}
|
||||||
|
component={Link}
|
||||||
|
to={path}
|
||||||
|
sx={listItemButtonStyle}
|
||||||
|
>
|
||||||
|
<StyledListItemIcon>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={path === '/projects'}
|
||||||
|
show={<StyledProjectIcon />}
|
||||||
|
elseShow={<IconRenderer path={path} />}
|
||||||
|
/>
|
||||||
|
</StyledListItemIcon>
|
||||||
|
<StyledListItemText>
|
||||||
|
<Typography color='textPrimary'>{name}</Typography>
|
||||||
|
</StyledListItemText>
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecentlyVisitedProjectButton = ({
|
||||||
|
projectId,
|
||||||
|
key,
|
||||||
|
}: { projectId: string; key: string }) => {
|
||||||
|
const { project, loading } = useProjectOverview(projectId);
|
||||||
|
const projectDeleted = !project.name && !loading;
|
||||||
|
if (projectDeleted) return null;
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
key={key}
|
||||||
|
dense={true}
|
||||||
|
component={Link}
|
||||||
|
to={`/projects/${projectId}`}
|
||||||
|
sx={listItemButtonStyle}
|
||||||
|
>
|
||||||
|
<StyledListItemIcon>
|
||||||
|
<StyledProjectIcon />
|
||||||
|
</StyledListItemIcon>
|
||||||
|
<StyledListItemText>
|
||||||
|
<Typography color='textPrimary'>{project.name}</Typography>
|
||||||
|
</StyledListItemText>
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecentlyVisited = ({
|
export const RecentlyVisited = ({
|
||||||
lastVisited,
|
lastVisited,
|
||||||
}: { lastVisited: LastViewedPage[] }) => {
|
routes,
|
||||||
const listItems = toListItemData(lastVisited);
|
}: {
|
||||||
|
lastVisited: LastViewedPage[];
|
||||||
|
routes: Record<string, { path: string; route: string; title: string }>;
|
||||||
|
}) => {
|
||||||
|
const buttons = lastVisited.map((item, index) =>
|
||||||
|
toListItemButton(item, routes, index),
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledTypography color='textSecondary'>
|
<StyledTypography color='textSecondary'>
|
||||||
Recently visited
|
Recently visited
|
||||||
</StyledTypography>
|
</StyledTypography>
|
||||||
<List>
|
<List>{buttons}</List>
|
||||||
{listItems.map((item, index) => (
|
|
||||||
<ListItemButton
|
|
||||||
key={`recently-visited-${index}`}
|
|
||||||
dense={true}
|
|
||||||
component={Link}
|
|
||||||
to={item.path}
|
|
||||||
sx={listItemButtonStyle}
|
|
||||||
>
|
|
||||||
<StyledListItemIcon>{item.icon}</StyledListItemIcon>
|
|
||||||
<StyledListItemText>
|
|
||||||
<Typography>{item.name}</Typography>
|
|
||||||
</StyledListItemText>
|
|
||||||
</ListItemButton>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,6 +12,7 @@ import UsersIcon from '@mui/icons-material/GroupOutlined';
|
|||||||
import ServiceAccountIcon from '@mui/icons-material/Computer';
|
import ServiceAccountIcon from '@mui/icons-material/Computer';
|
||||||
import GroupsIcon from '@mui/icons-material/GroupsOutlined';
|
import GroupsIcon from '@mui/icons-material/GroupsOutlined';
|
||||||
import RoleIcon from '@mui/icons-material/AdminPanelSettingsOutlined';
|
import RoleIcon from '@mui/icons-material/AdminPanelSettingsOutlined';
|
||||||
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import ApiAccessIcon from '@mui/icons-material/KeyOutlined';
|
import ApiAccessIcon from '@mui/icons-material/KeyOutlined';
|
||||||
import SingleSignOnIcon from '@mui/icons-material/AssignmentOutlined';
|
import SingleSignOnIcon from '@mui/icons-material/AssignmentOutlined';
|
||||||
import NetworkIcon from '@mui/icons-material/HubOutlined';
|
import NetworkIcon from '@mui/icons-material/HubOutlined';
|
||||||
@ -32,6 +33,7 @@ import { styled } from '@mui/material';
|
|||||||
|
|
||||||
// TODO: move to routes
|
// TODO: move to routes
|
||||||
const icons: Record<string, typeof SvgIcon> = {
|
const icons: Record<string, typeof SvgIcon> = {
|
||||||
|
'/search': SearchIcon,
|
||||||
'/applications': ApplicationsIcon,
|
'/applications': ApplicationsIcon,
|
||||||
'/context': ContextFieldsIcon,
|
'/context': ContextFieldsIcon,
|
||||||
'/feature-toggle-type': FlagTypesIcon,
|
'/feature-toggle-type': FlagTypesIcon,
|
||||||
|
Loading…
Reference in New Issue
Block a user