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

BED-5453: deep linking support for selected environment #1170

Open
wants to merge 70 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
02332ea
feat: useQueryParams in shared ui
benwaples Feb 13, 2025
11e000a
refactor: ui browser mocks and add deepLinking feature flag mock
benwaples Feb 19, 2025
c795fb1
chore: update import from mock
benwaples Feb 19, 2025
45b77fa
refactor: useExploreParams and useExploreGraph
benwaples Feb 19, 2025
f7d0312
feat: brute force the GraphViewV2 feature flag logic
benwaples Feb 19, 2025
8c5d147
chore: missing licenses
benwaples Feb 19, 2025
fe54dfd
chore: GraphViewFeatureToggle
benwaples Feb 19, 2025
053f97e
chore: add remaining explore params, documentation, remove nil, and o…
benwaples Feb 19, 2025
45f89f1
chore: better types and support arrays as query params
benwaples Feb 19, 2025
935915a
chore: remove test code
benwaples Feb 19, 2025
99315a4
chore: missing license and update comment
benwaples Feb 19, 2025
0124dc2
test: utils
benwaples Feb 19, 2025
c795cca
getExploreGraphQuery
benwaples Feb 19, 2025
0d733e0
chore: remove deleteNil param
benwaples Feb 19, 2025
de57404
chore: cleanup unused files
benwaples Feb 19, 2025
272dd71
chore: remove useQueryParams
benwaples Feb 19, 2025
3e2e1fe
refactor: remove nil type, use EmptyParam type, refactor double negative
benwaples Feb 20, 2025
eec3142
fix: useCallback definition
benwaples Feb 20, 2025
f5bacae
test: isEmptyParam
benwaples Feb 20, 2025
f80a020
fix: uppercase type
benwaples Feb 20, 2025
a20f443
initial: default domain and change domain to environment
benwaples Feb 21, 2025
7ddcfa9
initial: useAvailableEnvironment
benwaples Feb 21, 2025
fe2903c
fix: persistSearchParams
benwaples Feb 21, 2025
d3acbad
refactor: move GloballySupportSearchParam
benwaples Feb 22, 2025
18e2d0e
chore: adds useEnvironment
benwaples Feb 22, 2025
beb6868
feat: GroupManagementV2
benwaples Feb 22, 2025
707ba64
chore: dev note
benwaples Feb 24, 2025
85ff6e9
chore: merge main
benwaples Feb 24, 2025
4cc3d3f
fix: make persistentSearchParams optional
benwaples Feb 24, 2025
139a0a0
feat: GroupManagementContent
benwaples Feb 24, 2025
c36e492
feat: wire up QA env
benwaples Feb 25, 2025
c44622c
chore: file renaming
benwaples Feb 25, 2025
1e297d3
refactor: remove unused files
benwaples Feb 25, 2025
e54f678
fix: stop content feature flag running on login
benwaples Feb 25, 2025
4e9a5f1
feat: plumb options thru useFeatureFlag
benwaples Feb 25, 2025
b2d0463
chore: keep GraphViewV2 in sync with original
benwaples Feb 25, 2025
b54eceb
chore: more featureFlag checks
benwaples Feb 25, 2025
dfab064
Merge branch 'main' of github.com:SpecterOps/BloodHound into BED-5453
benwaples Feb 25, 2025
0c62069
feat: persist environmentId param on admin pages
benwaples Feb 25, 2025
b0fd02f
test: parseEnvironmentParams
benwaples Feb 25, 2025
a92a2a4
test: fix potential circular dep in useExploreParams
benwaples Feb 26, 2025
45c74bf
test: add history package for testing params and update test-utils fo…
benwaples Feb 26, 2025
cafd7e7
test: update nav tests to expect the router to be in the existing ren…
benwaples Feb 26, 2025
3852883
test: useEnvironment
benwaples Feb 26, 2025
a324cb9
test: useAvailableEnvironments and dont pass appendQueryKey to QueryO…
benwaples Feb 26, 2025
236330a
chore: clean up
benwaples Feb 26, 2025
29a5a2a
refactor: GroupManagementContentV2 is way too much
benwaples Feb 26, 2025
9e141ed
chore: remove GroupManagementV2
benwaples Feb 26, 2025
7c009c8
chore: simplier state switch
benwaples Feb 26, 2025
1314d81
docs: rename var
benwaples Feb 27, 2025
9fabe4e
chore: revert file changes
benwaples Feb 27, 2025
f82fe92
feat: useInitialEnvironment hook
benwaples Feb 27, 2025
405a966
refactor: remove duplicate content
benwaples Feb 27, 2025
a1c635e
chore: merge main and resolve conflicts in Admin and test utils
benwaples Feb 27, 2025
120a8dc
fix: missing type
benwaples Feb 27, 2025
a250852
fix: check for env id and type before requesting assetg group memebers
benwaples Feb 28, 2025
c4bcae6
chore: just generate clean up
benwaples Feb 28, 2025
9d9b013
chore: remove mock
benwaples Feb 28, 2025
fb12b71
style: rename supportedSearchParams and improve types a little
benwaples Feb 28, 2025
1930d2e
test: useInitialEnvironment
benwaples Feb 28, 2025
d4cd49e
chore: licenses
benwaples Feb 28, 2025
740d05e
Merge branch 'main' of github.com:SpecterOps/BloodHound into BED-5453
benwaples Feb 28, 2025
d49b5cb
docs: add some types and renmae supportedSearchParams
benwaples Feb 28, 2025
3420138
docs: add some types and renmae supportedSearchParams
benwaples Feb 28, 2025
4f087c0
fix: dont write over queryKey
benwaples Feb 28, 2025
184cec7
chore: rename param
benwaples Feb 28, 2025
2aa2cee
chore: just generate
benwaples Mar 3, 2025
64e97b5
refactor: Domain type renamed to Environment
benwaples Mar 3, 2025
340565f
refactor: a couple more Domain to Env
benwaples Mar 3, 2025
6b54730
Merge branch 'main' of github.com:SpecterOps/BloodHound into BED-5453
benwaples Mar 3, 2025
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
8 changes: 4 additions & 4 deletions cmd/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ import { fullyAuthenticatedSelector, initialize } from 'src/ducks/auth/authSlice
import { ROUTES } from 'src/routes';
import { useAppDispatch, useAppSelector } from 'src/store';
import { initializeBHEClient } from 'src/utils';
import Content from 'src/views/Content';
import {
MainNavPrimaryListData,
useMainNavLogoData,
useMainNavPrimaryListData,
useMainNavSecondaryListData,
} from './components/MainNav/MainNavData';
import Notifier from './components/Notifier';
import { setDarkMode } from './ducks/global/actions';
import ContentFeatureToggle from './views/ContentFeatureToggle';

export const Inner: React.FC = () => {
const dispatch = useAppDispatch();
Expand All @@ -56,7 +56,7 @@ export const Inner: React.FC = () => {
const featureFlagsRes = useFeatureFlags({ retry: false, enabled: !!authState.isInitialized && fullyAuthenticated });
const mainNavData = {
logo: useMainNavLogoData(),
primaryList: MainNavPrimaryListData,
primaryList: useMainNavPrimaryListData(),
secondaryList: useMainNavSecondaryListData(),
};
const showNavBar = useShowNavBar(ROUTES);
Expand Down Expand Up @@ -183,7 +183,7 @@ export const Inner: React.FC = () => {
<Box className={`${classes.applicationContainer}`} id='app-root'>
{showNavBar && <MainNav mainNavData={mainNavData} />}
<Box className={classes.applicationContent}>
<Content />
<ContentFeatureToggle />
</Box>
<AppNotifications />
<Notifier />
Expand Down
64 changes: 47 additions & 17 deletions cmd/ui/src/components/MainNav/MainNavData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@
// SPDX-License-Identifier: Apache-2.0

import { Switch } from '@bloodhoundenterprise/doodleui';
import { AppIcon } from 'bh-shared-ui';
import { logout } from 'src/ducks/auth/authSlice';
import { AppIcon, GloballySupportedSearchParams, useFeatureFlag } from 'bh-shared-ui';
import { fullyAuthenticatedSelector, logout } from 'src/ducks/auth/authSlice';
import { setDarkMode } from 'src/ducks/global/actions.ts';
import * as routes from 'src/routes/constants';
import { useAppDispatch, useAppSelector } from 'src/store';

export const useMainNavLogoData = () => {
const authState = useAppSelector((state) => state.auth);
const fullyAuthenticated = useAppSelector(fullyAuthenticatedSelector);
const { data: flag } = useFeatureFlag('back_button_support', {
enabled: !!authState.isInitialized && fullyAuthenticated,
});

const darkMode = useAppSelector((state) => state.global.view.darkMode);

const bhceImageUrlDarkMode = '/img/banner-ce-dark-mode.png';
const bhceImageUrlLightMode = '/img/banner-ce-light-mode.png';
const soImageUrlDarkMode = '/img/banner-so-dark-mode.png';
Expand All @@ -45,25 +52,41 @@ export const useMainNavLogoData = () => {
altText: 'SpecterOps Text Logo',
},
},
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
};
};

export const MainNavPrimaryListData = [
{
label: 'Explore',
icon: <AppIcon.LineChart size={24} />,
route: routes.ROUTE_EXPLORE,
testId: 'global_nav-explore',
},
{
label: 'Group Management',
icon: <AppIcon.Diamond size={24} />,
route: routes.ROUTE_GROUP_MANAGEMENT,
testId: 'global_nav-group-management',
},
];
export const useMainNavPrimaryListData = () => {
const authState = useAppSelector((state) => state.auth);
const fullyAuthenticated = useAppSelector(fullyAuthenticatedSelector);
const { data: flag } = useFeatureFlag('back_button_support', {
enabled: !!authState.isInitialized && fullyAuthenticated,
});
return [
{
label: 'Explore',
icon: <AppIcon.LineChart size={24} />,
route: routes.ROUTE_EXPLORE,
testId: 'global_nav-explore',
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
{
label: 'Group Management',
icon: <AppIcon.Diamond size={24} />,
route: routes.ROUTE_GROUP_MANAGEMENT,
testId: 'global_nav-group-management',
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
];
};

export const useMainNavSecondaryListData = () => {
const authState = useAppSelector((state) => state.auth);
const fullyAuthenticated = useAppSelector(fullyAuthenticatedSelector);
const { data: flag } = useFeatureFlag('back_button_support', {
enabled: !!authState.isInitialized && fullyAuthenticated,
});

const dispatch = useAppDispatch();
const darkMode = useAppSelector((state) => state.global.view.darkMode);

Expand All @@ -85,30 +108,35 @@ export const useMainNavSecondaryListData = () => {
icon: <AppIcon.User size={24} />,
route: routes.ROUTE_MY_PROFILE,
testId: 'global_nav-my-profile',
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
{
label: 'Download Collectors',
icon: <AppIcon.Download size={24} />,
route: routes.ROUTE_DOWNLOAD_COLLECTORS,
testId: 'global_nav-download-collectors',
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
{
label: 'Administration',
icon: <AppIcon.UserCog size={24} />,
route: routes.ROUTE_ADMINISTRATION_ROOT,
route: routes.DEFAULT_ADMINISTRATION_ROUTE,
testId: 'global_nav-administration',
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
{
label: 'API Explorer',
icon: <AppIcon.Compass size={24} />,
route: routes.ROUTE_API_EXPLORER,
testId: 'global_nav-api-explorer',
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
{
label: 'Docs and Support',
icon: <AppIcon.FileMagnifyingGlass size={24} />,
functionHandler: handleGoToSupport,
testId: 'global_nav-support',
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
{
label: (
Expand All @@ -120,12 +148,14 @@ export const useMainNavSecondaryListData = () => {
icon: <AppIcon.EclipseCircle size={24} />,
functionHandler: handleToggleDarkMode,
testId: 'global_nav-dark-mode',
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
{
label: 'Log Out',
icon: <AppIcon.Logout size={24} />,
functionHandler: handleLogout,
testId: 'global_nav-logout',
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
];
};
4 changes: 2 additions & 2 deletions cmd/ui/src/mocks/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// SPDX-License-Identifier: Apache-2.0

import { setupWorker } from 'msw';
// import handlers from './handlers';
import handlers from './handlers';

// This configures a Service Worker with the given request handlers.
export const worker = setupWorker();
export const worker = setupWorker(...handlers.deepLinking);
3 changes: 3 additions & 0 deletions cmd/ui/src/routes/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ export const ROUTE_ADMINISTRATION_SSO_CONFIGURATION = '/administration/sso-confi
export const ROUTE_ADMINISTRATION_EARLY_ACCESS_FEATURES = '/administration/early-access-features';
export const ROUTE_ADMINISTRATION_BLOODHOUND_CONFIGURATION = '/administration/bloodhound-configuration';
export const ROUTE_API_EXPLORER = '/api-explorer';

export const ENVIRONMENT_SUPPORTED_ROUTES = [ROUTE_GROUP_MANAGEMENT, ROUTE_ADMINISTRATION_DATA_QUALITY];
export const DEFAULT_ADMINISTRATION_ROUTE = ROUTE_ADMINISTRATION_FILE_INGEST;
4 changes: 2 additions & 2 deletions cmd/ui/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const UserProfile = React.lazy(() => import('bh-shared-ui').then((module) => ({
const DownloadCollectors = React.lazy(() => import('src/views/DownloadCollectors'));
const Administration = React.lazy(() => import('src/views/Administration'));
const ApiExplorer = React.lazy(() => import('bh-shared-ui').then((module) => ({ default: module.ApiExplorer })));
const GroupManagement = React.lazy(() => import('src/views/GroupManagement/GroupManagement'));
const GroupManagementFeatureToggle = React.lazy(() => import('src/views/GroupManagement/GroupManagementFeatureToggle'));

export const ROUTES = [
{
Expand Down Expand Up @@ -62,7 +62,7 @@ export const ROUTES = [
},
{
path: routes.ROUTE_GROUP_MANAGEMENT,
component: GroupManagement,
component: GroupManagementFeatureToggle,
authenticationRequired: true,
navigation: true,
},
Expand Down
40 changes: 20 additions & 20 deletions cmd/ui/src/views/Administration/Administration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
// SPDX-License-Identifier: Apache-2.0

import { Box, CircularProgress, Container } from '@mui/material';
import { GenericErrorBoundaryFallback, Permission, SubNav } from 'bh-shared-ui';
import { GenericErrorBoundaryFallback, GloballySupportedSearchParams, SubNav, useFeatureFlag } from 'bh-shared-ui';
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Navigate, Route, Routes } from 'react-router-dom';
import usePermissions from 'src/hooks/usePermissions/usePermissions';
import {
DEFAULT_ADMINISTRATION_ROUTE,
ROUTE_ADMINISTRATION_BLOODHOUND_CONFIGURATION,
ROUTE_ADMINISTRATION_DATA_QUALITY,
ROUTE_ADMINISTRATION_DB_MANAGEMENT,
Expand All @@ -29,6 +30,7 @@ import {
ROUTE_ADMINISTRATION_MANAGE_USERS,
ROUTE_ADMINISTRATION_SSO_CONFIGURATION,
} from 'src/routes/constants';
import { AdminSection, getAdminFilteredSections, getAdminSubRoute } from './utils';

const DatabaseManagement = React.lazy(() => import('src/views/DatabaseManagement'));
const QA = React.lazy(() => import('src/views/QA'));
Expand All @@ -41,7 +43,8 @@ const SSOConfiguration = React.lazy(() =>
);

const Administration: React.FC = () => {
const sections = [
const { data: flag } = useFeatureFlag('back_button_support');
const sections: AdminSection[] = [
{
title: 'Data Collection',
items: [
Expand All @@ -50,18 +53,21 @@ const Administration: React.FC = () => {
path: ROUTE_ADMINISTRATION_FILE_INGEST,
component: FileIngest,
adminOnly: false,
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
{
label: 'Data Quality',
path: ROUTE_ADMINISTRATION_DATA_QUALITY,
component: QA,
adminOnly: false,
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
{
label: 'Database Management',
path: ROUTE_ADMINISTRATION_DB_MANAGEMENT,
component: DatabaseManagement,
adminOnly: false,
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
],
order: 0,
Expand All @@ -74,6 +80,7 @@ const Administration: React.FC = () => {
path: ROUTE_ADMINISTRATION_MANAGE_USERS,
component: Users,
adminOnly: false,
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
],
order: 0,
Expand All @@ -86,6 +93,7 @@ const Administration: React.FC = () => {
path: ROUTE_ADMINISTRATION_SSO_CONFIGURATION,
component: SSOConfiguration,
adminOnly: false,
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
],
order: 0,
Expand All @@ -98,12 +106,14 @@ const Administration: React.FC = () => {
path: ROUTE_ADMINISTRATION_BLOODHOUND_CONFIGURATION,
component: BloodHoundConfiguration,
adminOnly: true,
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
{
label: 'Early Access Features',
path: ROUTE_ADMINISTRATION_EARLY_ACCESS_FEATURES,
component: EarlyAccessFeatures,
adminOnly: false,
persistentSearchParams: flag?.enabled ? GloballySupportedSearchParams : undefined,
},
],
order: 1,
Expand All @@ -112,23 +122,8 @@ const Administration: React.FC = () => {

const { checkAllPermissions } = usePermissions();

// Checking these for now because the only route we are currently hiding is to the configuration page.
// In practice, this will permit Administrators and Power User roles only.
const hasAdminPermissions = checkAllPermissions([
Permission.APP_READ_APPLICATION_CONFIGURATION,
Permission.APP_WRITE_APPLICATION_CONFIGURATION,
]);

// Filter adminOnly links from the data we pass to the sidebar if a user does not have the correct permissions
const adminFilteredSections = sections
.map((section) => {
const filteredItems = section.items.filter((item) => !item.adminOnly || hasAdminPermissions);
return {
...section,
items: filteredItems,
};
})
.filter((section) => section.items.length !== 0);
const adminFilteredSections = getAdminFilteredSections(sections, checkAllPermissions);

return (
<Box className='flex h-full pl-subnav-width'>
Expand Down Expand Up @@ -159,7 +154,7 @@ const Administration: React.FC = () => {
.reduce((acc, val) => acc.concat(val), [])
.map((item) => (
<Route
path={item.path.slice(16)}
path={getAdminSubRoute(item.path)}
key={item.path}
element={
<ErrorBoundary fallbackRender={GenericErrorBoundaryFallback}>
Expand All @@ -168,7 +163,12 @@ const Administration: React.FC = () => {
}
/>
))}
<Route path='*' element={<Navigate to='file-ingest' replace />} />
<Route
path='*'
element={
<Navigate to={getAdminSubRoute(DEFAULT_ADMINISTRATION_ROUTE)} replace />
}
/>
</Routes>
</Suspense>
</Box>
Expand Down
58 changes: 58 additions & 0 deletions cmd/ui/src/views/Administration/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2025 Specter Ops, Inc.
//
// Licensed under the Apache License, Version 2.0
// 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 { Permission } from 'bh-shared-ui';
import { FC, LazyExoticComponent } from 'react';

export const getAdminSubRoute = (route: string) => {
const administrationRoute = '/administration/';
return route.slice(administrationRoute.length);
};

interface AdminSectionItem {
label: string;
path: string;
component: LazyExoticComponent<FC>;
adminOnly: boolean;
persistentSearchParams?: string[];
}

export interface AdminSection {
title: string;
items: AdminSectionItem[];
order: number;
}

export const getAdminFilteredSections = (
sections: AdminSection[],
checkAllPermissions: (permissions: Permission[]) => boolean
) => {
// Checking these for now because the only route we are currently hiding is to the configuration page.
// In practice, this will permit Administrators and Power User roles only.
const hasAdminPermissions = checkAllPermissions([
Permission.APP_READ_APPLICATION_CONFIGURATION,
Permission.APP_WRITE_APPLICATION_CONFIGURATION,
]);
return sections
.map((section) => {
const filteredItems = section.items.filter((item) => !item.adminOnly || hasAdminPermissions);
return {
...section,
items: filteredItems,
};
})
.filter((section) => section.items.length !== 0);
};
Loading