Skip to content

Commit 5deebb9

Browse files
gregaubertsonartech
authored andcommitted
SONAR-26378 Create the new sidebar for the project/portfolio space (#3933)
GitOrigin-RevId: 74ace2c3648f55f67ce3cd84d6ab7dee1a4b15c9
1 parent d16bdc5 commit 5deebb9

File tree

34 files changed

+1760
-344
lines changed

34 files changed

+1760
-344
lines changed

apps/sq-server/src/main/js/app/components/ComponentContainer.tsx

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
2020

21-
import { Spinner } from '@sonarsource/echoes-react';
21+
import { Layout, Spinner } from '@sonarsource/echoes-react';
2222
import { differenceBy } from 'lodash';
2323
import * as React from 'react';
2424
import { createPortal } from 'react-dom';
@@ -31,6 +31,7 @@ import { useLocation, useRouter } from '~shared/components/hoc/withRouter';
3131
import { isFile, isPortfolioLike } from '~shared/helpers/component';
3232
import { isDefined } from '~shared/helpers/types';
3333
import { getProjectOverviewUrl } from '~shared/helpers/urls';
34+
import { useNewUI } from '~shared/helpers/useNewUI';
3435
import { ComponentQualifier } from '~shared/types/component';
3536
import { HttpStatus } from '~shared/types/request';
3637
import { validateProjectAlmBinding } from '~sq-server-commons/api/alm-settings';
@@ -51,22 +52,26 @@ import { Task, TaskStatuses, TaskTypes } from '~sq-server-commons/types/tasks';
5152
import { Component } from '~sq-server-commons/types/types';
5253
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
5354
import ComponentContainerNotFound from './ComponentContainerNotFound';
54-
import ComponentNav from './nav/component/ComponentNav';
55+
import { ComponentNav } from './nav/component/ComponentNav';
56+
import {
57+
LegacyComponentNav,
58+
LegacyComponentNavCompatibleWithNewLayout,
59+
} from './nav/component/legacy/ComponentNav';
5560

5661
const FETCH_STATUS_WAIT_TIME = 3000;
5762

5863
function ComponentContainer({ hasFeature }: Readonly<WithAvailableFeaturesProps>) {
5964
const watchStatusTimer = React.useRef<number>();
60-
const portalAnchor = React.useRef<Element | null>(null);
65+
const legacyComponentNavAnchor = React.useRef<Element | null>(); // undefined means it wansn't loaded yet, if not found it will be null
6166
const oldTasksInProgress = React.useRef<Task[]>();
6267
const oldCurrentTask = React.useRef<Task>();
6368
const {
6469
query: { id: key, branch, pullRequest, fixedInPullRequest },
6570
pathname,
6671
} = useLocation();
6772
const router = useRouter();
68-
6973
const intl = useIntl();
74+
const [newUI] = useNewUI();
7075

7176
const [component, setComponent] = React.useState<Component>();
7277
const [projectComponent, setProjectComponent] = React.useState<Component>();
@@ -318,7 +323,7 @@ function ComponentContainer({ hasFeature }: Readonly<WithAvailableFeaturesProps>
318323

319324
// Set portal anchor on mount
320325
React.useEffect(() => {
321-
portalAnchor.current = document.querySelector('#component-nav-portal');
326+
legacyComponentNavAnchor.current = document.querySelector('#component-nav-portal');
322327
}, []);
323328

324329
const isInProgress = tasksInProgress && tasksInProgress.length > 0;
@@ -349,6 +354,44 @@ function ComponentContainer({ hasFeature }: Readonly<WithAvailableFeaturesProps>
349354
return <ComponentContainerNotFound isPortfolioLike={pathname.includes('portfolio')} />;
350355
}
351356

357+
// TODO drop this once project scope migration is done
358+
const isLegacyLayout = legacyComponentNavAnchor.current !== null;
359+
if (isLegacyLayout) {
360+
return (
361+
<>
362+
<Helmet
363+
defer={false}
364+
titleTemplate={intl.formatMessage(
365+
{ id: 'page_title.template.with_instance' },
366+
{ project: component?.name ?? '' },
367+
)}
368+
/>
369+
{component &&
370+
!isFile(component.qualifier) &&
371+
legacyComponentNavAnchor.current &&
372+
/* Use a portal to fix positioning until we can fully review the layout */
373+
createPortal(
374+
<LegacyComponentNav
375+
component={component}
376+
isInProgress={isInProgress}
377+
isPending={isPending}
378+
projectBindingErrors={projectBindingErrors}
379+
/>,
380+
legacyComponentNavAnchor.current,
381+
)}
382+
{loading ? (
383+
<CenteredLayout>
384+
<Spinner className="sw-mt-10" />
385+
</CenteredLayout>
386+
) : (
387+
<ComponentContext.Provider value={componentProviderProps}>
388+
<Outlet />
389+
</ComponentContext.Provider>
390+
)}
391+
</>
392+
);
393+
}
394+
352395
return (
353396
<>
354397
<Helmet
@@ -358,28 +401,28 @@ function ComponentContainer({ hasFeature }: Readonly<WithAvailableFeaturesProps>
358401
{ project: component?.name ?? '' },
359402
)}
360403
/>
361-
{component &&
362-
!isFile(component.qualifier) &&
363-
portalAnchor.current &&
364-
/* Use a portal to fix positioning until we can fully review the layout */
365-
createPortal(
366-
<ComponentNav
404+
{newUI && component && !isFile(component.qualifier) && <ComponentNav component={component} />}
405+
<Layout.ContentGrid>
406+
{!newUI && component && !isFile(component.qualifier) && (
407+
<LegacyComponentNavCompatibleWithNewLayout
367408
component={component}
368409
isInProgress={isInProgress}
369410
isPending={isPending}
370411
projectBindingErrors={projectBindingErrors}
371-
/>,
372-
portalAnchor.current,
412+
/>
373413
)}
374-
{loading ? (
375-
<CenteredLayout>
376-
<Spinner className="sw-mt-10" />
377-
</CenteredLayout>
378-
) : (
379-
<ComponentContext.Provider value={componentProviderProps}>
380-
<Outlet />
381-
</ComponentContext.Provider>
382-
)}
414+
<Layout.PageGrid>
415+
<Layout.PageContent>
416+
{loading ? (
417+
<Spinner className="sw-mt-10" />
418+
) : (
419+
<ComponentContext.Provider value={componentProviderProps}>
420+
<Outlet />
421+
</ComponentContext.Provider>
422+
)}
423+
</Layout.PageContent>
424+
</Layout.PageGrid>
425+
</Layout.ContentGrid>
383426
</>
384427
);
385428
}

apps/sq-server/src/main/js/app/components/GlobalContainer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import NonProductionDatabaseWarning from './NonProductionDatabaseWarning';
3838
import SystemAnnouncement from './SystemAnnouncement';
3939
import EnableAiCodeFixMessage from './ai-codefix-notification/EnableAiCodeFixMessage';
4040
import CalculationChangeMessage from './calculation-notification/CalculationChangeMessage';
41-
import GlobalNav from './nav/global/GlobalNav';
41+
import { GlobalNav, GlobalNavLegacy } from './nav/global/GlobalNav';
4242
import PromotionNotification from './promotion-notification/PromotionNotification';
4343
import { UpdateNotification } from './update-notification/UpdateNotification';
4444

@@ -130,7 +130,7 @@ export default function GlobalContainer() {
130130
<SQSTemporaryRelativeBannerContainer>
131131
<Banners />
132132
</SQSTemporaryRelativeBannerContainer>
133-
<GlobalNav />
133+
<GlobalNavLegacy />
134134
{hasFeature(Feature.Architecture) && canAdmin && addons.architecture?.spotlight({})}
135135
<ModeTour />
136136
{/* The following is the portal anchor point for the component nav

apps/sq-server/src/main/js/app/components/Landing.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
import { Navigate, To } from 'react-router-dom';
2222
import withCurrentUserContext from '~sq-server-commons/context/current-user/withCurrentUserContext';
23-
import { getHomePageUrl } from '~sq-server-commons/helpers/urls';
23+
import { getHomePageUrl, getProjectsUrl } from '~sq-server-commons/helpers/urls';
2424
import { CurrentUser, isLoggedIn } from '~sq-server-commons/types/users';
2525

2626
export interface LandingProps {
@@ -32,7 +32,7 @@ export function Landing({ currentUser }: LandingProps) {
3232
if (isLoggedIn(currentUser) && currentUser.homepage) {
3333
redirectUrl = getHomePageUrl(currentUser.homepage);
3434
} else {
35-
redirectUrl = '/projects';
35+
redirectUrl = getProjectsUrl();
3636
}
3737

3838
return <Navigate replace to={redirectUrl} />;

apps/sq-server/src/main/js/app/components/NonAdminPagesContainer.tsx

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,34 @@
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
2020

21-
import * as React from 'react';
21+
import { MessageCallout } from '@sonarsource/echoes-react';
22+
import { useContext } from 'react';
23+
import { FormattedMessage } from 'react-intl';
2224
import { Outlet } from 'react-router-dom';
23-
import { CenteredLayout, FlagMessage } from '~design-system';
25+
import { CenteredLayout } from '~design-system';
2426
import { isApplication } from '~shared/helpers/component';
2527
import { ComponentContext } from '~sq-server-commons/context/componentContext/ComponentContext';
26-
import { translate } from '~sq-server-commons/helpers/l10n';
2728

2829
export default function NonAdminPagesContainer() {
29-
const { component } = React.useContext(ComponentContext);
30+
const { component } = useContext(ComponentContext);
3031

3132
/*
3233
* Catch Applications for which the user does not have access to all child projects
3334
* and prevent displaying whatever page was requested.
3435
* This doesn't apply to admin pages (those are not within this container)
3536
*/
36-
if (component && isApplication(component.qualifier) && !component.canBrowseAllChildProjects) {
37+
38+
const isApplicationChildInaccessible =
39+
component && isApplication(component.qualifier) && !component.canBrowseAllChildProjects;
40+
41+
if (isApplicationChildInaccessible) {
3742
return (
38-
<CenteredLayout
39-
className="sw-py-8 sw-typo-lg sw-flex sw-flex-col sw-items-center"
40-
id="code-page"
41-
>
42-
<FlagMessage className="it__alert-no-access-all-child-project sw-mt-10" variant="error">
43-
<p>
44-
{translate('application.cannot_access_all_child_projects1')}
45-
<br />
46-
{translate('application.cannot_access_all_child_projects2')}
47-
</p>
48-
</FlagMessage>
43+
<CenteredLayout className="sw-py-8 sw-typo-lg sw-flex sw-flex-col sw-items-center">
44+
<MessageCallout className="it__alert-no-access-all-child-project sw-mt-10" variety="danger">
45+
<FormattedMessage id="application.cannot_access_all_child_projects1" />
46+
<br />
47+
<FormattedMessage id="application.cannot_access_all_child_projects2" />
48+
</MessageCallout>
4949
</CenteredLayout>
5050
);
5151
}

apps/sq-server/src/main/js/app/components/__tests__/ComponentContainer-test.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ describe('getTasksForComponent', () => {
223223
});
224224

225225
// getTasksForComponent updated the tasks, which triggers the setTimeout
226-
expect(jest.getTimerCount()).toBe(1);
226+
expect(jest.getTimerCount()).toBeGreaterThan(0);
227227
// we run the timer to trigger the next getTasksForComponent call
228228
jest.runOnlyPendingTimers();
229229

@@ -270,7 +270,7 @@ describe('getTasksForComponent', () => {
270270
});
271271

272272
// Despite the fact taht we don't have any tasks in the queue, the component.analysisDate is undefined, so we trigger setTimeout
273-
expect(jest.getTimerCount()).toBe(1);
273+
expect(jest.getTimerCount()).toBeGreaterThan(0);
274274
jest.runOnlyPendingTimers();
275275

276276
// Second round, nothing in the queue, BUT a success task is current. This
@@ -283,12 +283,14 @@ describe('getTasksForComponent', () => {
283283
await waitFor(() => {
284284
expect(getComponentNavigation).toHaveBeenCalledTimes(2);
285285
});
286+
287+
// Make sure the timeout was cleared
288+
jest.runOnlyPendingTimers();
289+
expect(jest.getTimerCount()).toBe(0);
290+
286291
// The status API call will be called 1 final time after the component is
287292
// fully loaded, so the total will be 3.
288293
expect(getTasksForComponent).toHaveBeenCalledTimes(3);
289-
290-
// Make sure the timeout was cleared. It should not be called again.
291-
expect(jest.getTimerCount()).toBe(0);
292294
});
293295

294296
it('only fully loads a non-empty component once', async () => {
@@ -310,7 +312,9 @@ describe('getTasksForComponent', () => {
310312
});
311313

312314
// Since the component has analysisDate, and the queue is empty, the setTimeout will not be triggered
315+
jest.runOnlyPendingTimers();
313316
expect(jest.getTimerCount()).toBe(0);
317+
expect(getTasksForComponent).toHaveBeenCalledTimes(1);
314318
});
315319

316320
it('only fully reloads a non-empty component if there was previously some task in progress', async () => {
@@ -336,7 +340,7 @@ describe('getTasksForComponent', () => {
336340
expect(getTasksForComponent).toHaveBeenCalledTimes(1);
337341
});
338342

339-
expect(jest.getTimerCount()).toBe(1);
343+
expect(jest.getTimerCount()).toBeGreaterThan(0);
340344
jest.runOnlyPendingTimers();
341345

342346
// Second round, nothing in the queue, and a success task is current. This
@@ -350,12 +354,13 @@ describe('getTasksForComponent', () => {
350354
expect(getComponentNavigation).toHaveBeenCalledTimes(2);
351355
});
352356

357+
// Make sure the timeout was cleared.
358+
jest.runOnlyPendingTimers();
359+
expect(jest.getTimerCount()).toBe(0);
360+
353361
// The status API call will be called 1 final time after the component is
354362
// fully loaded, so the total will be 3.
355363
expect(getTasksForComponent).toHaveBeenCalledTimes(3);
356-
357-
// Make sure the timeout was cleared. It should not be called again.
358-
expect(jest.getTimerCount()).toBe(0);
359364
});
360365
});
361366

@@ -520,7 +525,7 @@ describe('tutorials', () => {
520525
expect(mockedReplace).not.toHaveBeenCalled();
521526

522527
// Since component.analysisDate is undefined we trigger setTimeout
523-
expect(jest.getTimerCount()).toBe(1);
528+
expect(jest.getTimerCount()).toBeGreaterThan(0);
524529
jest.runOnlyPendingTimers();
525530
expect(getTasksForComponent).toHaveBeenCalledTimes(2);
526531

@@ -572,7 +577,7 @@ describe('tutorials', () => {
572577
expect(mockedReplace).not.toHaveBeenCalled();
573578

574579
// Since component.analysisDate is undefined we trigger setTimeout
575-
expect(jest.getTimerCount()).toBe(1);
580+
expect(jest.getTimerCount()).toBeGreaterThan(0);
576581
jest.runOnlyPendingTimers();
577582
expect(getTasksForComponent).toHaveBeenCalledTimes(2);
578583

@@ -626,7 +631,7 @@ describe('tutorials', () => {
626631
expect(mockedReplace).not.toHaveBeenCalled();
627632

628633
// Since component.analysisDate is undefined we trigger setTimeout
629-
expect(jest.getTimerCount()).toBe(1);
634+
expect(jest.getTimerCount()).toBeGreaterThan(0);
630635
jest.runOnlyPendingTimers();
631636
expect(getTasksForComponent).toHaveBeenCalledTimes(2);
632637

@@ -662,7 +667,14 @@ function renderComponentContainer(
662667
renderAppRoutes(
663668
path,
664669
() => (
665-
<Route element={<ComponentContainer />}>
670+
<Route
671+
element={
672+
<>
673+
<div id="component-nav-portal" />
674+
<ComponentContainer />
675+
</>
676+
}
677+
>
666678
<Route element={<TestComponent />} path="*" />
667679
<Route element={<div>portfolio</div>} path="portfolio" />
668680
<Route element={<div>project</div>} path="dashboard" />

apps/sq-server/src/main/js/app/components/__tests__/ModeTour-test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { EditionKey } from '~sq-server-commons/types/editions';
2929
import { Permissions } from '~sq-server-commons/types/permissions';
3030
import { NoticeType } from '~sq-server-commons/types/users';
3131
import ModeTour from '../ModeTour';
32-
import GlobalNav from '../nav/global/GlobalNav';
32+
import { GlobalNavLegacy } from '../nav/global/GlobalNav';
3333

3434
const ui = {
3535
close: byRole('button', { name: 'modal.close' }),
@@ -219,7 +219,7 @@ function renderGlobalNav(currentUser = mockCurrentUser()) {
219219
renderApp(
220220
'/',
221221
<>
222-
<GlobalNav />
222+
<GlobalNavLegacy />
223223
<ModeTour />
224224
</>,
225225
{

apps/sq-server/src/main/js/app/components/global-search/GlobalSearch.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import * as React from 'react';
2424
import { FormattedMessage } from 'react-intl';
2525
import { DEBOUNCE_DELAY, DropdownMenu, Popup, PopupZLevel } from '~design-system';
2626
import { withRouter } from '~shared/components/hoc/withRouter';
27+
import { RecentHistory } from '~shared/helpers/recent-history';
2728
import { ComponentQualifier } from '~shared/types/component';
2829
import { Router } from '~shared/types/router';
2930
import { getSuggestions } from '~sq-server-commons/api/components';
@@ -35,7 +36,6 @@ import { KeyboardKeys } from '~sq-server-commons/helpers/keycodes';
3536
import { getIntl } from '~sq-server-commons/helpers/l10nBundle';
3637
import { getKeyboardShortcutEnabled } from '~sq-server-commons/helpers/preferences';
3738
import { getComponentOverviewUrl } from '~sq-server-commons/helpers/urls';
38-
import RecentHistory from '../RecentHistory';
3939
import { GlobalSearchResult } from './GlobalSearchResult';
4040
import GlobalSearchResults from './GlobalSearchResults';
4141
import { ComponentResult, More, Results, sortQualifiers } from './utils';

0 commit comments

Comments
 (0)