diff --git a/client/web/src/enterprise/batches/global/GlobalBatchChangesArea.tsx b/client/web/src/enterprise/batches/global/GlobalBatchChangesArea.tsx index e0c48a3aaf7..3da16af7fed 100644 --- a/client/web/src/enterprise/batches/global/GlobalBatchChangesArea.tsx +++ b/client/web/src/enterprise/batches/global/GlobalBatchChangesArea.tsx @@ -46,7 +46,6 @@ const BatchChangeClosePage = lazyComponent> = ({ authenticatedUser, - isSourcegraphDotCom, ...props }) => { + const isSourcegraphDotCom = Boolean(window.context?.sourcegraphDotComMode) const canCreate: true | string = useMemo(() => { if (isSourcegraphDotCom) { return NO_ACCESS_SOURCEGRAPH_COM @@ -77,7 +76,6 @@ export const GlobalBatchChangesArea: React.FunctionComponent } diff --git a/client/web/src/enterprise/batches/list/BatchChangeListPage.story.tsx b/client/web/src/enterprise/batches/list/BatchChangeListPage.story.tsx index 09fc4a43b44..f556897ebb0 100644 --- a/client/web/src/enterprise/batches/list/BatchChangeListPage.story.tsx +++ b/client/web/src/enterprise/batches/list/BatchChangeListPage.story.tsx @@ -1,5 +1,5 @@ -import type { Decorator, StoryFn, Meta } from '@storybook/react' -import { WildcardMockLink, MATCH_ANY_PARAMETERS } from 'wildcard-mock-link' +import type { Decorator, Meta, StoryFn } from '@storybook/react' +import { MATCH_ANY_PARAMETERS, WildcardMockLink } from 'wildcard-mock-link' import { getDocumentNode } from '@sourcegraph/http-client' import { EMPTY_SETTINGS_CASCADE } from '@sourcegraph/shared/src/settings/settings' @@ -11,10 +11,10 @@ import { WebStory } from '../../../components/WebStory' import type { GlobalChangesetsStatsResult } from '../../../graphql-operations' import { - GLOBAL_CHANGESETS_STATS, BATCH_CHANGES, BATCH_CHANGES_BY_NAMESPACE, GET_LICENSE_AND_USAGE_INFO, + GLOBAL_CHANGESETS_STATS, } from './backend' import { BatchChangeListPage } from './BatchChangeListPage' import { @@ -103,7 +103,6 @@ export const ListOfBatchChanges: StoryFn = args => { headingElement="h1" canCreate={args.canCreate || "You don't have permission to create batch changes"} settingsCascade={EMPTY_SETTINGS_CASCADE} - isSourcegraphDotCom={args.isDotCom} authenticatedUser={null} telemetryRecorder={noOpTelemetryRecorder} /> @@ -142,7 +141,6 @@ export const ListOfBatchChangesSpecificNamespace: StoryFn = () => { canCreate={true} namespaceID="test-12345" settingsCascade={EMPTY_SETTINGS_CASCADE} - isSourcegraphDotCom={false} authenticatedUser={null} telemetryRecorder={noOpTelemetryRecorder} /> @@ -171,7 +169,6 @@ export const ListOfBatchChangesServerSideExecutionEnabled: StoryFn = () => { experimentalFeatures: { batchChangesExecution: true }, }, }} - isSourcegraphDotCom={false} authenticatedUser={null} telemetryRecorder={noOpTelemetryRecorder} /> @@ -195,7 +192,6 @@ export const LicensingNotEnforced: StoryFn = () => { headingElement="h1" canCreate={true} settingsCascade={EMPTY_SETTINGS_CASCADE} - isSourcegraphDotCom={false} authenticatedUser={null} telemetryRecorder={noOpTelemetryRecorder} /> @@ -219,7 +215,6 @@ export const NoBatchChanges: StoryFn = () => { headingElement="h1" canCreate={true} settingsCascade={EMPTY_SETTINGS_CASCADE} - isSourcegraphDotCom={false} authenticatedUser={null} telemetryRecorder={noOpTelemetryRecorder} /> @@ -244,7 +239,6 @@ export const AllBatchChangesTabEmpty: StoryFn = () => { canCreate={true} openTab="batchChanges" settingsCascade={EMPTY_SETTINGS_CASCADE} - isSourcegraphDotCom={false} authenticatedUser={null} telemetryRecorder={noOpTelemetryRecorder} /> diff --git a/client/web/src/enterprise/batches/list/BatchChangeListPage.tsx b/client/web/src/enterprise/batches/list/BatchChangeListPage.tsx index b9f17ad32e7..2b294eef9fe 100644 --- a/client/web/src/enterprise/batches/list/BatchChangeListPage.tsx +++ b/client/web/src/enterprise/batches/list/BatchChangeListPage.tsx @@ -57,7 +57,6 @@ export interface BatchChangeListPageProps extends TelemetryProps, TelemetryV2Pro canCreate: true | string headingElement: 'h1' | 'h2' namespaceID?: Scalars['ID'] - isSourcegraphDotCom: boolean authenticatedUser: AuthenticatedUser | null /** For testing only. */ openTab?: SelectedTab @@ -78,7 +77,6 @@ export const BatchChangeListPage: React.FunctionComponent { const location = useLocation() @@ -92,6 +90,7 @@ export const BatchChangeListPage: React.FunctionComponent( openTab ?? (isSourcegraphDotCom || !canUseBatchChanges ? 'gettingStarted' : 'batchChanges') ) diff --git a/client/web/src/namespaces/NamespaceArea.tsx b/client/web/src/namespaces/NamespaceArea.tsx index a6c9a68df9d..bd23b946be8 100644 --- a/client/web/src/namespaces/NamespaceArea.tsx +++ b/client/web/src/namespaces/NamespaceArea.tsx @@ -1,4 +1,4 @@ -import { type PlatformContextProps } from '@sourcegraph/shared/src/platform/context' +import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context' import type { AuthenticatedUser } from '../auth' import type { BatchChangesProps } from '../batches' @@ -11,13 +11,10 @@ import type { NamespaceProps } from '.' */ export interface NamespaceAreaContext extends PlatformContextProps, NamespaceProps { authenticatedUser: AuthenticatedUser | null - isSourcegraphDotCom: boolean } export interface NamespaceAreaRoute extends RouteV6Descriptor {} -interface NavItemDescriptorContext extends BatchChangesProps { - isSourcegraphDotCom: boolean -} +interface NavItemDescriptorContext extends BatchChangesProps {} export interface NamespaceAreaNavItem extends NavItemWithIconDescriptor {} diff --git a/client/web/src/namespaces/routes.tsx b/client/web/src/namespaces/routes.tsx index e99ede0ab44..066413cb148 100644 --- a/client/web/src/namespaces/routes.tsx +++ b/client/web/src/namespaces/routes.tsx @@ -22,7 +22,7 @@ export const namespaceAreaRoutes: readonly NamespaceAreaRoute[] = [ const SavedSearchesRedirect: FunctionComponent = ({ namespace }) => { const navigate = useNavigate() useEffect(() => { - navigate(urlToSavedSearchesList(namespace.id)) + navigate(urlToSavedSearchesList(namespace.id), { replace: true }) }, [navigate, namespace.id]) return null } diff --git a/client/web/src/org/OrgsArea.tsx b/client/web/src/org/OrgsArea.tsx index 064e8b00213..372fee4551f 100644 --- a/client/web/src/org/OrgsArea.tsx +++ b/client/web/src/org/OrgsArea.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { Routes, Route, useParams, useLocation, useNavigate } from 'react-router-dom' +import { Route, Routes } from 'react-router-dom' import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context' import type { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings' @@ -9,10 +9,10 @@ import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetry import type { AuthenticatedUser } from '../auth' import { withAuthenticatedUser } from '../auth/withAuthenticatedUser' import type { BatchChangesProps } from '../batches' -import type { BreadcrumbsProps, BreadcrumbSetters } from '../components/Breadcrumbs' +import type { BreadcrumbSetters, BreadcrumbsProps } from '../components/Breadcrumbs' import { NotFoundPage } from '../components/HeroPage' -import { OrgArea, type OrgAreaProps, type OrgAreaRoute } from './area/OrgArea' +import { OrgArea, type OrgAreaRoute } from './area/OrgArea' import type { OrgAreaHeaderNavItem } from './area/OrgHeader' import { OrgInvitationPage } from './invitations/OrgInvitationPage' import { NewOrganizationPage } from './new/NewOrganizationPage' @@ -32,7 +32,6 @@ export interface Props orgSettingsAreaRoutes: readonly OrgSettingsAreaRoute[] authenticatedUser: AuthenticatedUser - isSourcegraphDotCom: boolean } /** @@ -40,28 +39,17 @@ export interface Props */ const AuthenticatedOrgsArea: React.FunctionComponent> = props => ( - {(!props.isSourcegraphDotCom || props.authenticatedUser.siteAdmin) && ( - } - /> - )} + } + /> } /> - } /> + } /> } /> ) -// TODO: Migrate this into the OrgArea component once it's migrated to a function component. -function OrgAreaWithRouteProps(props: Omit): JSX.Element { - const { orgName } = useParams<{ orgName: string }>() - const location = useLocation() - const navigate = useNavigate() - - return -} - export const OrgsArea = withAuthenticatedUser(AuthenticatedOrgsArea) diff --git a/client/web/src/org/area/OrgArea.tsx b/client/web/src/org/area/OrgArea.tsx index 045e494c2a2..4527d42e506 100644 --- a/client/web/src/org/area/OrgArea.tsx +++ b/client/web/src/org/area/OrgArea.tsx @@ -1,14 +1,10 @@ -import * as React from 'react' +import React, { Suspense, useMemo } from 'react' -import type * as H from 'history' import AlertCircleIcon from 'mdi-react/AlertCircleIcon' import MapSearchIcon from 'mdi-react/MapSearchIcon' -import { Route, Routes, type NavigateFunction } from 'react-router-dom' -import { Subject, Subscription, combineLatest, merge, of, type Observable } from 'rxjs' -import { catchError, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators' +import { Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom' -import { asError, isErrorLike, logger, type ErrorLike } from '@sourcegraph/common' -import { dataOrThrowErrors, gql } from '@sourcegraph/http-client' +import { gql, useQuery } from '@sourcegraph/http-client' import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context' import type { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings' import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' @@ -16,68 +12,20 @@ import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetry import { ErrorMessage, LoadingSpinner } from '@sourcegraph/wildcard' import type { AuthenticatedUser } from '../../auth' -import { requestGraphQL } from '../../backend/graphql' import type { BatchChangesProps } from '../../batches' import type { BreadcrumbSetters, BreadcrumbsProps } from '../../components/Breadcrumbs' import { RouteError } from '../../components/ErrorBoundary' import { HeroPage } from '../../components/HeroPage' import { Page } from '../../components/Page' -import type { OrgAreaOrganizationFields, OrganizationResult, OrganizationVariables } from '../../graphql-operations' +import type { OrgAreaOrganizationFields } from '../../graphql-operations' import type { NamespaceProps } from '../../namespaces' import type { RouteV6Descriptor } from '../../util/contributions' import type { OrgSettingsAreaRoute } from '../settings/OrgSettingsArea' import type { OrgSettingsSidebarItems } from '../settings/OrgSettingsSidebar' -import { OrgHeader, type OrgAreaHeaderNavItem } from './OrgHeader' +import { type OrgAreaHeaderNavItem, OrgHeader } from './OrgHeader' import { OrgInvitationPageLegacy } from './OrgInvitationPageLegacy' -function queryOrganization(args: { name: string }): Observable { - return requestGraphQL( - gql` - query Organization($name: String!) { - organization(name: $name) { - ...OrgAreaOrganizationFields - } - } - - fragment OrgAreaOrganizationFields on Org { - __typename - id - name - displayName - url - settingsURL - viewerPendingInvitation { - id - sender { - username - displayName - avatarURL - createdAt - } - respondURL - } - viewerIsMember - viewerCanAdminister - createdAt - } - `, - args - ).pipe( - map(dataOrThrowErrors), - map(data => { - if (!data.organization) { - throw new Error(`Organization not found: ${JSON.stringify(args.name)}`) - } - return data.organization - }) - ) -} - -const NotFoundPage: React.FunctionComponent> = () => ( - -) - export interface OrgAreaRoute extends RouteV6Descriptor { /** When true, the header is not rendered and the component is not wrapped in a container. */ fullPage?: boolean @@ -99,19 +47,41 @@ export interface OrgAreaProps * The currently authenticated user. */ authenticatedUser: AuthenticatedUser - isSourcegraphDotCom: boolean - - location: H.Location - navigate: NavigateFunction - orgName: string } -interface State extends BreadcrumbSetters { - /** - * The fetched org or an error if an error occurred; undefined while loading. - */ - orgOrError?: OrgAreaOrganizationFields | ErrorLike -} +const ORGANIZATION_QUERY = gql` + query Organization($name: String!) { + organization(name: $name) { + ...OrgAreaOrganizationFields + } + } + + fragment OrgAreaOrganizationFields on Org { + __typename + id + name + displayName + url + settingsURL + viewerPendingInvitation { + id + sender { + username + displayName + avatarURL + createdAt + } + respondURL + } + viewerIsMember + viewerCanAdminister + createdAt + } +` + +const NotFoundPage: React.FunctionComponent> = () => ( + +) /** * Properties passed to all page components in the org area. @@ -134,164 +104,116 @@ export interface OrgAreaRouteContext /** The currently authenticated user. */ authenticatedUser: AuthenticatedUser - isSourcegraphDotCom: boolean - orgSettingsSideBarItems: OrgSettingsSidebarItems orgSettingsAreaRoutes: readonly OrgSettingsAreaRoute[] } -/** - * An organization's public profile area. - */ -export class OrgArea extends React.Component { - public state: State - - private componentUpdates = new Subject() - private refreshRequests = new Subject() - private subscriptions = new Subscription() - - constructor(props: OrgAreaProps) { - super(props) - this.state = { - setBreadcrumb: props.setBreadcrumb, - useBreadcrumb: props.useBreadcrumb, - } +export const OrgArea: React.FunctionComponent = ({ + orgAreaRoutes, + orgAreaHeaderNavItems, + orgSettingsSideBarItems, + orgSettingsAreaRoutes, + authenticatedUser, + useBreadcrumb, + ...props +}) => { + const { orgName } = useParams<{ orgName: string }>() + if (!orgName) { + throw new Error('orgName is required') } - public componentDidMount(): void { - // Changes to the route-matched org name. - const nameChanges = this.componentUpdates.pipe( - map(props => props.orgName), - distinctUntilChanged() + const navigate = useNavigate() + const location = useLocation() + + const { data, error, loading, refetch } = useQuery(ORGANIZATION_QUERY, { + variables: { name: orgName }, + }) + + const childBreadcrumbSetters = useBreadcrumb( + useMemo( + () => + data?.organization + ? { + key: 'OrgArea', + link: { to: data.organization.url, label: data.organization.name }, + } + : null, + [data] ) + ) - // Fetch organization. - this.subscriptions.add( - combineLatest([nameChanges, merge(this.refreshRequests.pipe(map(() => false)), of(true))]) - .pipe( - switchMap(([name, forceRefresh]) => { - type PartialStateUpdate = Pick - return queryOrganization({ name }).pipe( - catchError((error): [ErrorLike] => [asError(error)]), - map((orgOrError): PartialStateUpdate => ({ orgOrError })), - // Don't clear old org data while we reload, to avoid unmounting all components during - // loading. - startWith(forceRefresh ? { orgOrError: undefined } : {}) - ) - }) - ) - .subscribe( - stateUpdate => { - if (stateUpdate.orgOrError && !isErrorLike(stateUpdate.orgOrError)) { - const childBreadcrumbSetters = this.props.setBreadcrumb({ - key: 'OrgArea', - link: { to: stateUpdate.orgOrError.url, label: stateUpdate.orgOrError.name }, - }) - this.subscriptions.add(childBreadcrumbSetters) - this.setState({ - useBreadcrumb: childBreadcrumbSetters.useBreadcrumb, - setBreadcrumb: childBreadcrumbSetters.setBreadcrumb, - orgOrError: stateUpdate.orgOrError, - }) - } else { - this.setState(stateUpdate) - } - }, - error => logger.error(error) - ) - ) - - this.componentUpdates.next(this.props) + if (loading && !data) { + return } - public componentDidUpdate(): void { - this.componentUpdates.next(this.props) + if (error) { + return } /> } - public componentWillUnmount(): void { - this.subscriptions.unsubscribe() + if (!data?.organization) { + return } - public render(): JSX.Element | null { - if (!this.state.orgOrError) { - return null // loading - } - if (isErrorLike(this.state.orgOrError)) { - return ( - } - /> - ) - } - - const context: OrgAreaRouteContext = { - authenticatedUser: this.props.authenticatedUser, - org: this.state.orgOrError, - onOrganizationUpdate: this.onDidUpdateOrganization, - platformContext: this.props.platformContext, - settingsCascade: this.props.settingsCascade, - namespace: this.state.orgOrError, - telemetryService: this.props.telemetryService, - telemetryRecorder: this.props.platformContext.telemetryRecorder, - isSourcegraphDotCom: this.props.isSourcegraphDotCom, - batchChangesEnabled: this.props.batchChangesEnabled, - batchChangesExecutionEnabled: this.props.batchChangesExecutionEnabled, - batchChangesWebhookLogsEnabled: this.props.batchChangesWebhookLogsEnabled, - breadcrumbs: this.props.breadcrumbs, - setBreadcrumb: this.state.setBreadcrumb, - useBreadcrumb: this.state.useBreadcrumb, - orgSettingsAreaRoutes: this.props.orgSettingsAreaRoutes, - orgSettingsSideBarItems: this.props.orgSettingsSideBarItems, - } - - if (this.props.location.pathname === `/organizations/${this.props.orgName}/invitation`) { - // The OrgInvitationPageLegacy is displayed without the OrgHeader because it is modal-like. - return - } - - return ( - }> - - {this.props.orgAreaRoutes.map( - ({ path, render, condition = () => true, fullPage }) => - condition(context) && ( - } - element={ - fullPage ? ( - render(context) - ) : ( - - -
{render(context)}
-
- ) - } - /> - ) - )} - } /> -
-
- ) + const context: OrgAreaRouteContext = { + authenticatedUser, + org: data.organization, + onOrganizationUpdate: refetch, + platformContext: props.platformContext, + settingsCascade: props.settingsCascade, + namespace: data.organization, + telemetryService: props.telemetryService, + telemetryRecorder: props.platformContext.telemetryRecorder, + batchChangesEnabled: props.batchChangesEnabled, + batchChangesExecutionEnabled: props.batchChangesExecutionEnabled, + batchChangesWebhookLogsEnabled: props.batchChangesWebhookLogsEnabled, + breadcrumbs: props.breadcrumbs, + ...childBreadcrumbSetters, + orgSettingsAreaRoutes, + orgSettingsSideBarItems, } - private onDidRespondToInvitation = (accepted: boolean): void => { + const handleRespondToInvitation = (accepted: boolean): void => { if (!accepted) { - this.props.navigate('/user/settings') + navigate('/user/settings') return } - this.refreshRequests.next() + refetch() } - private onDidUpdateOrganization = (): void => this.refreshRequests.next() + if (location.pathname === `/organizations/${orgName}/invitation`) { + return + } + + return ( + }> + + {orgAreaRoutes.map( + ({ path, render, condition = () => true, fullPage }) => + condition(context) && ( + } + element={ + fullPage ? ( + render(context) + ) : ( + + +
{render(context)}
+
+ ) + } + /> + ) + )} + } /> +
+
+ ) } diff --git a/client/web/src/org/area/OrgHeader.tsx b/client/web/src/org/area/OrgHeader.tsx index cce6b45efa2..b2d218e7ded 100644 --- a/client/web/src/org/area/OrgHeader.tsx +++ b/client/web/src/org/area/OrgHeader.tsx @@ -11,14 +11,11 @@ import { OrgAvatar } from '../OrgAvatar' import type { OrgAreaRouteContext } from './OrgArea' interface Props extends OrgAreaRouteContext { - isSourcegraphDotCom: boolean navItems: readonly OrgAreaHeaderNavItem[] className?: string } -export interface OrgAreaHeaderContext extends BatchChangesProps, Pick { - isSourcegraphDotCom: boolean -} +export interface OrgAreaHeaderContext extends BatchChangesProps, Pick {} export interface OrgAreaHeaderNavItem extends NavItemWithIconDescriptor {} @@ -32,14 +29,12 @@ export const OrgHeader: React.FunctionComponent> org, navItems, className = '', - isSourcegraphDotCom, }) => { const context: OrgAreaHeaderContext = { batchChangesEnabled, batchChangesExecutionEnabled, batchChangesWebhookLogsEnabled, org, - isSourcegraphDotCom, } const url = `/organizations/${org.name}` diff --git a/client/web/src/org/area/routes.tsx b/client/web/src/org/area/routes.tsx index ada63984d08..4ca2d20ac3a 100644 --- a/client/web/src/org/area/routes.tsx +++ b/client/web/src/org/area/routes.tsx @@ -51,12 +51,12 @@ export const orgAreaRoutes: readonly OrgAreaRoute[] = [ // Redirect from /organizations/:orgname -> /organizations/:orgname/settings/profile. { path: '', - render: () => , + render: () => , }, // Redirect from previous /organizations/:orgname/account -> /organizations/:orgname/settings/profile. { path: 'account', - render: () => , + render: () => , }, { diff --git a/client/web/src/org/settings/OrgSettingsSidebar.tsx b/client/web/src/org/settings/OrgSettingsSidebar.tsx index 56d783eac93..10e3413a16e 100644 --- a/client/web/src/org/settings/OrgSettingsSidebar.tsx +++ b/client/web/src/org/settings/OrgSettingsSidebar.tsx @@ -19,7 +19,6 @@ import styles from './OrgSettingsSidebar.module.scss' export interface OrgSettingsSidebarItemConditionContext extends BatchChangesProps { org: OrgAreaOrganizationFields authenticatedUser: AuthenticatedUser - isSourcegraphDotCom: boolean } type OrgSettingsSidebarItem = NavItemDescriptor & { @@ -30,7 +29,6 @@ export type OrgSettingsSidebarItems = readonly OrgSettingsSidebarItem[] export interface OrgSettingsSidebarProps extends OrgSettingsAreaRouteContext, BatchChangesProps { items: OrgSettingsSidebarItems - isSourcegraphDotCom: boolean className?: string } @@ -53,7 +51,6 @@ export const OrgSettingsSidebar: React.FunctionComponent ), - condition: ({ org: { viewerCanAdminister } }) => - viewerCanAdminister && window.context?.codeSearchEnabledOnInstance, + condition: ({ batchChangesEnabled, org: { viewerCanAdminister } }) => + batchChangesEnabled && viewerCanAdminister && window.context?.codeSearchEnabledOnInstance, }, ]