From 9eee97d7d99ff735ae6c1d051c9b8295ad61e8fe Mon Sep 17 00:00:00 2001 From: Nick Lanam <314133+NickLanam@users.noreply.github.com> Date: Wed, 9 Aug 2023 10:48:56 -0700 Subject: [PATCH] Show the same sidebar everywhere (#1607) 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. image 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 --- .../live/keyboard-shortcuts.spec.ts | 2 + .../cypress/integration/live/navbars.spec.ts | 4 +- src/ui/src/api/api-client.ts | 2 +- src/ui/src/api/cloud-gql-client.ts | 2 +- src/ui/src/containers/App/sidebar.tsx | 163 +++++- src/ui/src/pages/admin/admin.tsx | 92 ++-- src/ui/src/pages/auth/logout.tsx | 2 +- src/ui/src/pages/auth/mock-oauth-provider.ts | 51 ++ src/ui/src/pages/auth/oauth-provider.ts | 4 +- src/ui/src/pages/live/live.tsx | 27 +- .../setup/__snapshots__/setup-test.tsx.snap | 466 ++++++++++++++++-- src/ui/src/testing/jest-test-setup.js | 11 +- 12 files changed, 661 insertions(+), 165 deletions(-) create mode 100644 src/ui/src/pages/auth/mock-oauth-provider.ts diff --git a/src/ui/cypress/integration/live/keyboard-shortcuts.spec.ts b/src/ui/cypress/integration/live/keyboard-shortcuts.spec.ts index 15a82404b07..d327bacc5fe 100644 --- a/src/ui/cypress/integration/live/keyboard-shortcuts.spec.ts +++ b/src/ui/cypress/integration/live/keyboard-shortcuts.spec.ts @@ -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); diff --git a/src/ui/cypress/integration/live/navbars.spec.ts b/src/ui/cypress/integration/live/navbars.spec.ts index 1c006e8a05e..cb3d9849649 100644 --- a/src/ui/cypress/integration/live/navbars.spec.ts +++ b/src/ui/cypress/integration/live/navbars.spec.ts @@ -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'); }); }); diff --git a/src/ui/src/api/api-client.ts b/src/ui/src/api/api-client.ts index 6d58c2a445f..785de9a2ba1 100644 --- a/src/ui/src/api/api-client.ts +++ b/src/ui/src/api/api-client.ts @@ -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'; @@ -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 diff --git a/src/ui/src/api/cloud-gql-client.ts b/src/ui/src/api/cloud-gql-client.ts index 7357967cf6b..95f1a7c09ed 100644 --- a/src/ui/src/api/cloud-gql-client.ts +++ b/src/ui/src/api/cloud-gql-client.ts @@ -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 }) => ({ diff --git a/src/ui/src/containers/App/sidebar.tsx b/src/ui/src/containers/App/sidebar.tsx index 96e8ec95e2b..3371e1b5373 100644 --- a/src/ui/src/containers/App/sidebar.tsx +++ b/src/ui/src/containers/App/sidebar.tsx @@ -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, @@ -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, @@ -188,19 +203,134 @@ const SideBarExternalLinkItem = React.memo(({ }); SideBarExternalLinkItem.displayName = 'SideBarExternalLinkItem'; +type ClusterRowInfo = Pick; + +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([]); + 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: , + link: deepLinkURLFromScript('px/cluster', selectedClusterName, embedState ?? defaultEmbedState, {}), + text: 'Cluster', + active: scriptId === 'px/cluster', + }, + { + icon: , + 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: , + link: '/admin/clusters', + text: 'Clusters', + active: pathname.endsWith('/admin/clusters'), + }, + { + icon: , + link: '/admin/keys/api', + text: 'Keys', + active: pathname.includes('/admin/keys'), + }, + { + icon: , + link: '/admin/plugins', + text: 'Plugins', + active: pathname.endsWith('/admin/plugins'), + }, + { + icon: , + link: '/admin/users', + text: 'Users', + active: pathname.endsWith('/admin/users'), + }, + { + icon: , + link: '/admin/org', + text: 'Org Settings', + active: pathname.endsWith('/admin/org'), + }, + { + icon: , + link: '/admin/user', + text: 'User Settings', + active: pathname.endsWith('/admin/user'), + }, + ...(showInvitations ? [{ + icon: , + link: '/admin/invite', + text: 'Invitations', + active: pathname.endsWith('/admin/invite'), + }] : []), + ] : [], [pathname, allowAdmin, showInvitations]); + + const pluginItems = React.useMemo(() => [ + { icon: , 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 }), @@ -222,19 +352,14 @@ export const SideBar: React.FC<{ open: boolean, buttons?: LinkItemProps[] }> = R - {buttons.length > 0 && ( - - {buttons.map((props) => ( + {groupedItems.map((g, i) => ( + + {i > 0 && ()} + {g.map((props) => ( ))} - - )} - {buttons.length > 0 && } - - {pluginItems.map((props) => ( - - ))} - + + ))}
{ diff --git a/src/ui/src/pages/admin/admin.tsx b/src/ui/src/pages/admin/admin.tsx index 17c2d4c6b5a..2814224d947 100644 --- a/src/ui/src/pages/admin/admin.tsx +++ b/src/ui/src/pages/admin/admin.tsx @@ -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'; @@ -88,75 +78,49 @@ const useStyles = makeStyles((theme: Theme) => createStyles({ export const AdminPage: React.FC = 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: , - link: '/admin/clusters', - text: 'Clusters', - active: pathname.endsWith('/admin/clusters'), - }, - { - icon: , - link: '/admin/keys/api', - text: 'Keys', - active: pathname.includes('/admin/keys'), - }, - { - icon: , - link: '/admin/plugins', - text: 'Plugins', - active: pathname.endsWith('/admin/plugins'), - }, - { - icon: , - link: '/admin/users', - text: 'Users', - active: pathname.endsWith('/admin/users'), - }, - { - icon: , - link: '/admin/org', - text: 'Org Settings', - active: pathname.endsWith('/admin/org'), - }, - { - icon: , - link: '/admin/user', - text: 'User Settings', - active: pathname.endsWith('/admin/user'), - }, - ...(showInvitations ? [{ - icon: , - 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 ? (
Admin - {active.text} + {titleLabel}
) : (
Admin
); - }, [classes.titleText, classes.titleDivider, sidebarButtons]); + }, [classes.titleText, classes.titleDivider, titleLabel]); return (
- +
{titleText}
diff --git a/src/ui/src/pages/auth/logout.tsx b/src/ui/src/pages/auth/logout.tsx index 776755de08b..412968338a8 100644 --- a/src/ui/src/pages/auth/logout.tsx +++ b/src/ui/src/pages/auth/logout.tsx @@ -20,11 +20,11 @@ import * as React from 'react'; import Axios from 'axios'; +import { GetCSRFCookie } from 'app/pages/auth/utils'; import pixieAnalytics from 'app/utils/analytics'; import * as RedirectUtils from 'app/utils/redirect-utils'; import { BasePage } from './base'; -import { GetCSRFCookie } from './utils'; // eslint-disable-next-line react-memo/require-memo export const LogoutPage: React.FC = () => { diff --git a/src/ui/src/pages/auth/mock-oauth-provider.ts b/src/ui/src/pages/auth/mock-oauth-provider.ts new file mode 100644 index 00000000000..f7c31060b39 --- /dev/null +++ b/src/ui/src/pages/auth/mock-oauth-provider.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2018- The Pixie Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { FormStructure } from 'app/components'; + +import { OAuthProviderClient } from './oauth-provider'; + +const mockForm: FormStructure = { + submitBtnText: 'Mock Submit', + action: 'https://mock.form.local', + fields: [], + method: 'POST', +}; + +export const MockOAuthClient: OAuthProviderClient = { + refetchToken() {}, + + handleToken: () => Promise.resolve({ + redirectArgs: {}, + token: {}, + }), + + getPasswordLoginFlow: () => Promise.resolve(mockForm), + + getResetPasswordFlow: () => Promise.resolve(mockForm), + + getError: () => Promise.resolve(mockForm), + + isInvitationEnabled: () => true, + + getInvitationComponent: () => undefined, + + getLoginButtons: () => null, + + getSignupButtons: () => null, +}; diff --git a/src/ui/src/pages/auth/oauth-provider.ts b/src/ui/src/pages/auth/oauth-provider.ts index 0b8020d136c..7fc6a4dc794 100644 --- a/src/ui/src/pages/auth/oauth-provider.ts +++ b/src/ui/src/pages/auth/oauth-provider.ts @@ -46,8 +46,8 @@ export interface OAuthProviderClient { getInvitationComponent(): React.FC | undefined; /** Gets the login buttons for this OAuthProvider. */ - getLoginButtons(): React.ReactElement; + getLoginButtons(): React.ReactNode; /** Gets the signup buttons for this OAuthProvider. */ - getSignupButtons(): React.ReactElement; + getSignupButtons(): React.ReactNode; } diff --git a/src/ui/src/pages/live/live.tsx b/src/ui/src/pages/live/live.tsx index 34aff629b2f..048e22e27ad 100644 --- a/src/ui/src/pages/live/live.tsx +++ b/src/ui/src/pages/live/live.tsx @@ -29,7 +29,7 @@ import { useFlags } from 'launchdarkly-react-client-sdk'; import { PixieAPIClient, PixieAPIContext } from 'app/api'; import { ClusterContext } from 'app/common/cluster-context'; import { isPixieEmbedded } from 'app/common/embed-context'; -import { ClusterIcon, EditIcon, Footer, NamespaceIcon, scrollbarStyles } from 'app/components'; +import { EditIcon, Footer, scrollbarStyles } from 'app/components'; import { CommandPalette } from 'app/components/command-palette'; import { CommandPaletteContext, @@ -41,7 +41,6 @@ import { LiveRouteContext } from 'app/containers/App/live-routing'; import { LiveTourContextProvider } from 'app/containers/App/live-tour'; import NavBars from 'app/containers/App/nav-bars'; import { SCRATCH_SCRIPT, ScriptsContext } from 'app/containers/App/scripts-context'; -import { LinkItemProps } from 'app/containers/App/sidebar'; import { DataDrawerSplitPanel } from 'app/containers/data-drawer/data-drawer'; import { EditorSplitPanel } from 'app/containers/editor/editor'; import { LiveViewBreadcrumbs } from 'app/containers/live/breadcrumbs'; @@ -51,7 +50,6 @@ import ExecuteScriptButton from 'app/containers/live/execute-button'; import { ScriptLoader } from 'app/containers/live/script-loader'; import ShareButton from 'app/containers/live/share-button'; import LiveViewShortcutsProvider from 'app/containers/live/shortcuts'; -import { deepLinkURLFromScript } from 'app/containers/live-widgets/utils/live-view-params'; import { SetStateFunc } from 'app/context/common'; import { DataDrawerContextProvider } from 'app/context/data-drawer-context'; import { EditorContext, EditorContextProvider } from 'app/context/editor-context'; @@ -297,33 +295,12 @@ const Nav: React.FC<{ const classes = useStyles(); const { showCommandPalette } = useFlags(); - const selectedClusterName = React.useContext(ClusterContext)?.selectedClusterName ?? ''; - const embedState = React.useContext(LiveRouteContext)?.embedState ?? null; - - const sidebarButtons: LinkItemProps[] = React.useMemo(() => { - if (!selectedClusterName || !embedState) { - return []; - } - return [ - { - icon: , - link: deepLinkURLFromScript('px/cluster', selectedClusterName, embedState, {}), - text: 'Cluster', - }, - { - icon: , - link: deepLinkURLFromScript('px/namespaces', selectedClusterName, embedState, {}), - text: 'Namespaces', - }, - ]; - }, [embedState, selectedClusterName]); - if (isPixieEmbedded()) { return <>; } return <> - +
{showCommandPalette && } diff --git a/src/ui/src/pages/setup/__snapshots__/setup-test.tsx.snap b/src/ui/src/pages/setup/__snapshots__/setup-test.tsx.snap index 94090f18687..e3b65479e06 100644 --- a/src/ui/src/pages/setup/__snapshots__/setup-test.tsx.snap +++ b/src/ui/src/pages/setup/__snapshots__/setup-test.tsx.snap @@ -101,82 +101,450 @@ exports[`setup page renders 1`] = ` />
-
+
- - + class="MuiTypography-root MuiTypography-body1 MuiListItemText-primary css-10hburv-MuiTypography-root" + > + Data Retention + +
+ +