Skip to content

Commit

Permalink
Show the same sidebar everywhere (#1607)
Browse files Browse the repository at this point in the history
Summary: Instead of showing and hiding parts depending on where you are.
We don't have a huge number of items in this bar, so it makes sense to
always have all of them.
In the admin and data retention views, it will pick the default cluster
for the script links.
<img width="288" alt="image"
src="https://github.com/pixie-io/pixie/assets/314133/da37daa5-753d-4959-b1ab-fc17197eab4d">

Relevant Issues: N/A

Type of change: /kind bugfix

Test Plan: Load the live views, the admin pages, and the data retention
view.
All of them should keep the same sidebar, and whichever view you're on
should be highlighted.

---------

Signed-off-by: Nick Lanam <[email protected]>
  • Loading branch information
NickLanam authored Aug 9, 2023
1 parent 656daaf commit 9eee97d
Show file tree
Hide file tree
Showing 12 changed files with 661 additions and 165 deletions.
2 changes: 2 additions & 0 deletions src/ui/cypress/integration/live/keyboard-shortcuts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ describe('Live view keyboard shortcuts', () => {
});

it('Re-runs the current script', () => {
// Give the page a moment to stabilize, so the run button can be enabled again (hard to find, so just wait on time).
cy.wait(500);
stubExecuteScript().as('repeat-exec'); // Not the original run (already waited on)
const hotkey = `${useCmdKey ? '{cmd}' : '{ctrl}'}{enter}`;
cy.get('body').type(hotkey);
Expand Down
4 changes: 2 additions & 2 deletions src/ui/cypress/integration/live/navbars.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ describe('Live View navbars', () => {
describe('Sidebar', () => {
beforeEach(() => {
cy.get('.MuiToolbar-root [aria-label="menu"]').as('sidebar-toggle');
cy.get('header + .MuiDrawer-root').as('sidebar').within(() => {
cy.get('ul:nth-child(2)').as('sidebar-scripts');
cy.get('header + .MuiDrawer-root > .MuiPaper-root').as('sidebar').within(() => {
cy.get('> a').as('sidebar-scripts');
cy.get('ul:last-child').as('sidebar-footer');
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/ui/src/api/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import fetch from 'cross-fetch';
import { Observable, from } from 'rxjs';
import { switchMap } from 'rxjs/operators';

import { GetCSRFCookie } from 'app/pages/auth/utils';
import { Status } from 'app/types/generated/vizierapi_pb';
import { containsMutation } from 'app/utils/pxl';

Expand All @@ -34,7 +35,6 @@ import {
VizierQueryFunc,
ExecuteScriptOptions,
} from './vizier-grpc-client';
import { GetCSRFCookie } from '../pages/auth/utils';

/**
* When calling `PixieAPIClient.create`, this specifies which clusters to connect to, and any special configuration for
Expand Down
2 changes: 1 addition & 1 deletion src/ui/src/api/cloud-gql-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ import { CachePersistor } from 'apollo3-cache-persist';
import fetch from 'cross-fetch';

import { isPixieEmbedded } from 'app/common/embed-context';
import { GetCSRFCookie } from 'app/pages/auth/utils';

import { PixieAPIClientOptions } from './api-options';
import { GetCSRFCookie } from '../pages/auth/utils';

// Apollo link that adds cookies in the request.
const makeCloudAuthLink = (opts: PixieAPIClientOptions) => setContext((_, { headers }) => ({
Expand Down
163 changes: 144 additions & 19 deletions src/ui/src/containers/App/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@

import * as React from 'react';

import { useQuery, gql } from '@apollo/client';
import {
Help as HelpIcon,
AccountTree as OrgIcon,
Campaign as CampaignIcon,
Extension as PluginIcon,
Group as UsersIcon,
Help as HelpIcon,
Person as UserIcon,
Send as InviteIcon,
VpnKey as KeyIcon,
} from '@mui/icons-material';
import {
Divider,
Expand All @@ -34,18 +41,26 @@ import {
import { Theme } from '@mui/material/styles';
import { createStyles, makeStyles } from '@mui/styles';
import AnnounceKit from 'announcekit-react';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';

import { ClusterContext } from 'app/common/cluster-context';
import { isPixieEmbedded } from 'app/common/embed-context';
import UserContext from 'app/common/user-context';
import { buildClass, DataDisksIcon, DocsIcon } from 'app/components';
import { buildClass, ClusterIcon, DataDisksIcon, DocsIcon, NamespaceIcon } from 'app/components';
import {
DOMAIN_NAME, ANNOUNCEMENT_ENABLED,
ANNOUNCE_WIDGET_URL,
} from 'app/containers/constants';
import { deepLinkURLFromScript } from 'app/containers/live-widgets/utils/live-view-params';
import { ScriptContext } from 'app/context/script-context';
import { GetOAuthProvider } from 'app/pages/auth/utils';
import { GQLClusterInfo } from 'app/types/schema';
import { showIntercomTrigger, triggerID } from 'app/utils/intercom';
import { SidebarFooter } from 'configurable/sidebar-footer';

import { selectClusterName } from './cluster-info';
import { LiveRouteContext } from './live-routing';

const useStyles = makeStyles(({
spacing,
palette,
Expand Down Expand Up @@ -188,19 +203,134 @@ const SideBarExternalLinkItem = React.memo<LinkItemProps>(({
});
SideBarExternalLinkItem.displayName = 'SideBarExternalLinkItem';

type ClusterRowInfo = Pick<GQLClusterInfo, 'id' | 'clusterName' | 'status'>;

const useSelectedOrDefaultClusterName = () => {
const { data } = useQuery<{
clusters: ClusterRowInfo[]
}>(
gql`
query listClustersForSidebar {
clusters {
id
clusterName
status
}
}
`,
{ pollInterval: 60000, fetchPolicy: 'cache-and-network' },
);

const [clusters, setClusters] = React.useState<ClusterRowInfo[]>([]);
React.useEffect(() => {
if (data?.clusters) {
setClusters(data.clusters);
}
}, [data?.clusters]);


const defaultClusterName = selectClusterName(clusters ?? []) ?? '';
const selectedClusterName = React.useContext(ClusterContext)?.selectedClusterName ?? '';

return selectedClusterName || defaultClusterName;
};

export const SideBar: React.FC<{ open: boolean, buttons?: LinkItemProps[] }> = React.memo(({ open, buttons = [] }) => {
const classes = useStyles();
const selectedClusterName = React.useContext(ClusterContext)?.selectedClusterName ?? '';

// Most sidebar items need some state or are conditional, so here are all of the contexts they need at once.
const selectedClusterName = useSelectedOrDefaultClusterName();
const embedState = React.useContext(LiveRouteContext)?.embedState;
const authClient = React.useMemo(() => GetOAuthProvider(), []);
const showInvitations = authClient.isInvitationEnabled();
const scriptId = React.useContext(ScriptContext)?.script?.id;
const { pathname } = useLocation();

const isEmbedded = isPixieEmbedded();
const defaultEmbedState = React.useMemo(() => ({
isEmbedded,
disableTimePicker: false,
widget: null,
}), [isEmbedded]);

const { user } = React.useContext(UserContext);

const pluginItems = React.useMemo(() => {
return [{
const liveItems: LinkItemProps[] = React.useMemo(() => {
if (!selectedClusterName) {
return [];
}
return [
{
icon: <ClusterIcon />,
link: deepLinkURLFromScript('px/cluster', selectedClusterName, embedState ?? defaultEmbedState, {}),
text: 'Cluster',
active: scriptId === 'px/cluster',
},
{
icon: <NamespaceIcon />,
link: deepLinkURLFromScript('px/namespaces', selectedClusterName, embedState ?? defaultEmbedState, {}),
text: 'Namespaces',
active: scriptId === 'px/namespaces',
},
];
}, [defaultEmbedState, embedState, scriptId, selectedClusterName]);

const allowAdmin = React.useMemo(() => !pathname.includes('/live'), [pathname]);
const adminItems: LinkItemProps[] = React.useMemo(() => allowAdmin ? [
{
icon: <ClusterIcon />,
link: '/admin/clusters',
text: 'Clusters',
active: pathname.endsWith('/admin/clusters'),
},
{
icon: <KeyIcon />,
link: '/admin/keys/api',
text: 'Keys',
active: pathname.includes('/admin/keys'),
},
{
icon: <PluginIcon />,
link: '/admin/plugins',
text: 'Plugins',
active: pathname.endsWith('/admin/plugins'),
},
{
icon: <UsersIcon />,
link: '/admin/users',
text: 'Users',
active: pathname.endsWith('/admin/users'),
},
{
icon: <OrgIcon />,
link: '/admin/org',
text: 'Org Settings',
active: pathname.endsWith('/admin/org'),
},
{
icon: <UserIcon />,
link: '/admin/user',
text: 'User Settings',
active: pathname.endsWith('/admin/user'),
},
...(showInvitations ? [{
icon: <InviteIcon />,
link: '/admin/invite',
text: 'Invitations',
active: pathname.endsWith('/admin/invite'),
}] : []),
] : [], [pathname, allowAdmin, showInvitations]);

const pluginItems = React.useMemo(() => [
{
icon: <DataDisksIcon />,
link: '/configure-data-export',
text: 'Data Retention',
}];
}, []);
active: pathname.includes('/configure-data-export'),
},
], [pathname]);

const groupedItems = [liveItems, adminItems, pluginItems, buttons].filter(g => g?.length > 0);

const drawerClasses = React.useMemo(
() => ({ paper: open ? classes.drawerOpen : classes.drawerClose }),
Expand All @@ -222,19 +352,14 @@ export const SideBar: React.FC<{ open: boolean, buttons?: LinkItemProps[] }> = R
<List>
<ListItem button className={classes.clippedItem} />
</List>
{buttons.length > 0 && (
<List>
{buttons.map((props) => (
{groupedItems.map((g, i) => (
<React.Fragment key={i}>
{i > 0 && (<Divider variant='middle' className={classes.divider} />)}
{g.map((props) => (
<SideBarInternalLinkItem key={props.text} {...props} />
))}
</List>
)}
{buttons.length > 0 && <Divider variant='middle' className={classes.divider} />}
<List>
{pluginItems.map((props) => (
<SideBarInternalLinkItem key={props.text} {...props} />
))}
</List>
</React.Fragment>
))}
<div className={classes.spacer} />
<List>
{
Expand Down
92 changes: 28 additions & 64 deletions src/ui/src/pages/admin/admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,16 @@

import * as React from 'react';

import {
AccountTree as OrgIcon,
Extension as PluginIcon,
Group as UsersIcon,
Person as UserIcon,
Send as InviteIcon,
VpnKey as KeyIcon,
} from '@mui/icons-material';
import { alpha } from '@mui/material';
import { Theme } from '@mui/material/styles';
import { createStyles, makeStyles } from '@mui/styles';
import { Route, Switch, Redirect, useLocation } from 'react-router-dom';

import { ClusterIcon, scrollbarStyles, Footer } from 'app/components';
import { scrollbarStyles, Footer } from 'app/components';
import { AdminOverview } from 'app/containers/admin/admin-overview';
import { LiveViewButton } from 'app/containers/admin/utils';
import NavBars from 'app/containers/App/nav-bars';
import { LinkItemProps } from 'app/containers/App/sidebar';
import { SidebarContext } from 'app/context/sidebar-context';
import { GetOAuthProvider } from 'app/pages/auth/utils';
import { WithChildren } from 'app/utils/react-boilerplate';
import { Copyright } from 'configurable/copyright';

Expand Down Expand Up @@ -88,75 +78,49 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
export const AdminPage: React.FC<WithChildren> = React.memo(({ children }) => {
const classes = useStyles();

// To determine if every tab is actually allowed
const authClient = React.useMemo(() => GetOAuthProvider(), []);
const showInvitations = authClient.isInvitationEnabled();

const { pathname } = useLocation();
const sidebarButtons: LinkItemProps[] = React.useMemo(() => [
{
icon: <ClusterIcon />,
link: '/admin/clusters',
text: 'Clusters',
active: pathname.endsWith('/admin/clusters'),
},
{
icon: <KeyIcon />,
link: '/admin/keys/api',
text: 'Keys',
active: pathname.includes('/admin/keys'),
},
{
icon: <PluginIcon />,
link: '/admin/plugins',
text: 'Plugins',
active: pathname.endsWith('/admin/plugins'),
},
{
icon: <UsersIcon />,
link: '/admin/users',
text: 'Users',
active: pathname.endsWith('/admin/users'),
},
{
icon: <OrgIcon />,
link: '/admin/org',
text: 'Org Settings',
active: pathname.endsWith('/admin/org'),
},
{
icon: <UserIcon />,
link: '/admin/user',
text: 'User Settings',
active: pathname.endsWith('/admin/user'),
},
...(showInvitations ? [{
icon: <InviteIcon />,
link: '/admin/invite',
text: 'Invitations',
active: pathname.endsWith('/admin/invite'),
}] : []),
], [pathname, showInvitations]);

const titleLabel = React.useMemo(() => {
const tabSlugs = pathname.split('/');
const tabSlug = tabSlugs[tabSlugs.indexOf('admin') + 1];
switch (tabSlug) {
case 'clusters':
return 'Clusters';
case 'keys':
return 'Keys';
case 'plugins':
return 'Plugins';
case 'users':
return 'Users';
case 'org':
return 'Org Settings';
case 'user':
return 'User Settings';
case 'invite':
return 'Invitations';
default:
return '';
}
}, [pathname]);

const titleText = React.useMemo(() => {
const active = sidebarButtons.find(b => b.active);
return active?.text ? (
return titleLabel ? (
<div className={classes.titleText}>
<span>Admin</span>
<span className={classes.titleDivider} />
<span>{active.text}</span>
<span>{titleLabel}</span>
</div>
) : (
<div className={classes.titleText}>
<span>Admin</span>
</div>
);
}, [classes.titleText, classes.titleDivider, sidebarButtons]);
}, [classes.titleText, classes.titleDivider, titleLabel]);

return (
<div className={classes.root}>
<SidebarContext.Provider value={{ showLiveOptions: false, showAdmin: true }}>
<NavBars sidebarButtons={sidebarButtons}>
<NavBars>
<div className={classes.title}>
{titleText}
</div>
Expand Down
Loading

0 comments on commit 9eee97d

Please sign in to comment.