Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: command bar pages and name resolving #7397

Merged
merged 3 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion frontend/src/component/commandBar/CommandBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
CommandResultGroup,
type CommandResultGroupItem,
} from './RecentlyVisited/CommandResultGroup';
import { PageSuggestions } from './PageSuggestions';
import { useRoutes } from 'component/layout/MainLayout/NavigationSidebar/useRoutes';

export const CommandResultsPaper = styled(Paper)(({ theme }) => ({
position: 'absolute',
Expand Down Expand Up @@ -84,6 +86,23 @@ export const CommandBar = () => {
const searchContainerRef = useRef<HTMLInputElement>(null);
const [showSuggestions, setShowSuggestions] = useState(false);
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 = () => {
setShowSuggestions(false);
};
Expand Down Expand Up @@ -186,7 +205,11 @@ export const CommandBar = () => {
elseShow={
showSuggestions && (
<CommandResultsPaper className='dropdown-outline'>
<RecentlyVisited lastVisited={lastVisited} />
<RecentlyVisited
lastVisited={lastVisited}
routes={allRoutes}
/>
<PageSuggestions routes={allRoutes} />
</CommandResultsPaper>
)
}
Expand Down
89 changes: 89 additions & 0 deletions frontend/src/component/commandBar/PageSuggestions.tsx
Original file line number Diff line number Diff line change
@@ -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>
</>
);
};
154 changes: 114 additions & 40 deletions frontend/src/component/commandBar/RecentlyVisited/RecentlyVisited.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
} from 'component/layout/MainLayout/NavigationSidebar/IconRenderer';
import type { LastViewedPage } from 'hooks/useRecentlyVisited';
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) => ({
borderRadius: theme.spacing(0.5),
Expand All @@ -37,55 +39,127 @@ const StyledListItemText = styled(ListItemText)(({ theme }) => ({
margin: 0,
}));

const toListItemData = (lastVisited: LastViewedPage[]) => {
return lastVisited.map((item) => {
if (item.featureId) {
return {
name: item.featureId,
path: `/projects/${item.projectId}/features/${item.featureId}`,
icon: <Icon>{'flag'}</Icon>,
};
}
if (item.projectId) {
return {
name: item.projectId,
path: `/projects/${item.projectId}`,
icon: <StyledProjectIcon />,
};
}
return {
name: item.featureId ?? item.projectId ?? item.pathName,
path: item.pathName || '/',
icon: <IconRenderer path={item.pathName ?? '/unknown'} />,
};
});
const toListItemButton = (
item: LastViewedPage,
routes: Record<string, { path: string; route: string; title: string }>,
index: number,
) => {
const key = `recently-visited-${index}`;
if (item.featureId && item.projectId) {
return (
<RecentlyVisitedFeatureButton
key={key}
featureId={item.featureId}
projectId={item.projectId}
/>
);
}
if (item.projectId) {
return (
<RecentlyVisitedProjectButton
key={key}
projectId={item.projectId}
/>
);
}
if (!item.pathName) return null;
const name = routes[item.pathName]?.title ?? item.pathName;
return (
<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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm downside of this is that every time they open the menu, we will make 1-5 concurrent calls to backend. But for now it is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, unfortunately

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 = ({
lastVisited,
}: { lastVisited: LastViewedPage[] }) => {
const listItems = toListItemData(lastVisited);
routes,
}: {
lastVisited: LastViewedPage[];
routes: Record<string, { path: string; route: string; title: string }>;
}) => {
const buttons = lastVisited.map((item, index) =>
toListItemButton(item, routes, index),
);
return (
<>
<StyledTypography color='textSecondary'>
Recently visited
</StyledTypography>
<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>
<List>{buttons}</List>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import UsersIcon from '@mui/icons-material/GroupOutlined';
import ServiceAccountIcon from '@mui/icons-material/Computer';
import GroupsIcon from '@mui/icons-material/GroupsOutlined';
import RoleIcon from '@mui/icons-material/AdminPanelSettingsOutlined';
import SearchIcon from '@mui/icons-material/Search';
import ApiAccessIcon from '@mui/icons-material/KeyOutlined';
import SingleSignOnIcon from '@mui/icons-material/AssignmentOutlined';
import NetworkIcon from '@mui/icons-material/HubOutlined';
Expand All @@ -32,6 +33,7 @@ import { styled } from '@mui/material';

// TODO: move to routes
const icons: Record<string, typeof SvgIcon> = {
'/search': SearchIcon,
'/applications': ApplicationsIcon,
'/context': ContextFieldsIcon,
'/feature-toggle-type': FlagTypesIcon,
Expand Down
Loading