/** * Copyright (c) 2020-present, Goldman Sachs * * 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. */ import { observer } from 'mobx-react-lite'; import { ACTIVITY_MODE, EDITOR_MODE, PANEL_MODE, USER_JOURNEYS, } from '../../stores/editor/EditorConfig.js'; import { LEGEND_STUDIO_TEST_ID } from '../../__lib__/LegendStudioTesting.js'; import { clsx, ControlledDropdownMenu, RepoIcon, MenuContent, MenuContentItem, MenuContentItemBlankIcon, MenuContentItemLabel, GitPullRequestIcon, GitMergeIcon, CloudDownloadIcon, CogIcon, CodeBranchIcon, EmptyClockIcon, WrenchIcon, FileTrayIcon, MenuIcon, MenuContentDivider, FlaskIcon, RobotIcon, WorkflowIcon, ReadMeIcon, DevIcon, MoonIcon, SunIcon, } from '@finos/legend-art'; import { useEditorStore } from './EditorStoreProvider.js'; import { forwardRef, useEffect, useState } from 'react'; import { LEGEND_APPLICATION_COLOR_THEME, ReleaseLogManager, ReleaseNotesManager, VIRTUAL_ASSISTANT_TAB, } from '@finos/legend-application'; /** * Themes that studio has real SCSS support for. Filters the global theme * registry (which also exposes stubs and app-specific themes like * `legacy-light`) down to what studio can actually render correctly. * * To add a new theme to studio's picker: * 1. Implement its SCSS in `legend-art/style/base/themes/` * 2. Add the key here. */ const STUDIO_SUPPORTED_COLOR_THEMES: ReadonlySet = new Set([ LEGEND_APPLICATION_COLOR_THEME.DEFAULT_DARK, LEGEND_APPLICATION_COLOR_THEME.DEFAULT_LIGHT, ]); import { LegendStudioAppInfo } from '../LegendStudioAppInfo.js'; import { generateSetupRoute } from '../../__lib__/LegendStudioNavigation.js'; import { useLegendStudioApplicationStore } from '../LegendStudioFrameworkProvider.js'; import { ActivityBarItemExperimentalBadge, type ActivityBarItemConfig, } from '@finos/legend-lego/application'; import { ShowcaseManagerState, openShowcaseManager, } from '../../stores/ShowcaseManagerState.js'; import { SHOWCASE_MANAGER_ENTRY_POINT } from '../../__lib__/LegendStudioTelemetryHelper.js'; import { toggleShowcasePanel } from './ShowcaseSideBar.js'; import { useAuth, type AuthContextProps } from 'react-oidc-context'; const SettingsMenu = observer( forwardRef(function SettingsMenu(props, ref) { const editorStore = useEditorStore(); const showDeveloperTool = (): void => { editorStore.panelGroupDisplayState.open(); editorStore.setActivePanelMode(PANEL_MODE.DEV_TOOL); }; return ( Show Developer Tool ); }), ); const useOptionalAuth = (): AuthContextProps | undefined => { try { return useAuth(); } catch { return undefined; } }; /** * One-click toggle between the default dark and default light color themes, * surfaced directly in the activity bar so users don't have to dig through the * Settings menu to flip themes. * * The toggle only renders when both themes are actually exposed by the * `STUDIO_SUPPORTED_COLOR_THEMES` allow-list; otherwise it would be a no-op. */ export const ColorThemeToggle = observer(() => { const applicationStore = useLegendStudioApplicationStore(); const { layoutService } = applicationStore; const exposedThemeKeys = new Set( layoutService.availableColorThemes .filter((theme) => STUDIO_SUPPORTED_COLOR_THEMES.has(theme.key)) .map((theme) => theme.key), ); // Only show the toggle when both endpoints of the dark<->light flip are // actually available; otherwise it would be a no-op control. if ( !exposedThemeKeys.has(LEGEND_APPLICATION_COLOR_THEME.DEFAULT_DARK) || !exposedThemeKeys.has(LEGEND_APPLICATION_COLOR_THEME.DEFAULT_LIGHT) ) { return null; } const isLight = layoutService.TEMPORARY__isLightColorThemeEnabled; const toggleTheme = (): void => { layoutService.setColorTheme( isLight ? LEGEND_APPLICATION_COLOR_THEME.DEFAULT_DARK : LEGEND_APPLICATION_COLOR_THEME.DEFAULT_LIGHT, { persist: true }, ); }; return ( ); }); export const ActivityBarMenu: React.FC<{ openShowcasePanel?: () => void; }> = ({ openShowcasePanel }) => { const applicationStore = useLegendStudioApplicationStore(); const auth = useOptionalAuth(); const appDocUrl = applicationStore.documentationService.url; const docLinks = applicationStore.documentationService.links; // about modal const [openAppInfo, setOpenAppInfo] = useState(false); const showAppInfo = (): void => setOpenAppInfo(true); const hideAppInfo = (): void => setOpenAppInfo(false); // documentation const goToDocumentation = (): void => { if (appDocUrl) { applicationStore.navigationService.navigator.visitAddress(appDocUrl); } }; const goToDocLink = (url: string): void => { applicationStore.navigationService.navigator.visitAddress(url); }; // go to setup page const goToWorkspaceSetup = (): void => applicationStore.navigationService.navigator.visitAddress( applicationStore.navigationService.navigator.generateAddress( generateSetupRoute(undefined, undefined), ), ); // help const openHelp = (): void => { applicationStore.assistantService.setIsHidden(false); applicationStore.assistantService.setIsOpen(true); applicationStore.assistantService.setSelectedTab( VIRTUAL_ASSISTANT_TAB.SEARCH, ); }; // showcases const showcaseManagerState = ShowcaseManagerState.retrieveNullableState(applicationStore); // release notification useEffect(() => { if (auth === undefined || auth.isAuthenticated) { applicationStore.releaseNotesService.updateViewedVersion(); } }, [applicationStore, auth, auth?.isAuthenticated]); return ( <>
About {showcaseManagerState?.isEnabled && ( openShowcasePanel ? openShowcasePanel() : openShowcaseManager( applicationStore, SHOWCASE_MANAGER_ENTRY_POINT.ACTIVITY_BAR, ) } > See Showcases )} Documentation {docLinks?.map((entry) => ( goToDocLink(entry.url)} > {entry.label} ))} Help... Back to workspace setup } >
); }; export const ActivityBar = observer(() => { const editorStore = useEditorStore(); const changeActivity = (activity: string): (() => void) => (): void => editorStore.setActiveActivity(activity); // local changes const localChanges = editorStore.changeDetectionState.workspaceLocalLatestRevisionState.changes .length; const localChangesDisplayLabel = localChanges > 99 ? '99+' : localChanges; const localChangesIndicatorStatusIcon = editorStore.graphManagerState.graphBuildState.hasFailed || editorStore.changeDetectionState.initState.hasFailed ? (
) : !editorStore.changeDetectionState.initState.hasSucceeded || editorStore.changeDetectionState.workspaceLocalLatestRevisionState .isBuildingEntityHashesIndex || editorStore.localChangesState.pushChangesState.isInProgress ? (
) : localChanges ? (
{localChangesDisplayLabel}
) : (
); // conflict resolution changes const conflictResolutionChanges = editorStore.conflictResolutionState.conflicts.length + editorStore.conflictResolutionState.changes.length; const conflictResolutionConflicts = editorStore.conflictResolutionState.conflicts.length; const conflictResolutionChangesDisplayLabel = conflictResolutionChanges > 99 ? '99+' : conflictResolutionChanges; const conflictResolutionChangesIndicatorStatusIcon = !editorStore.isInConflictResolutionMode ? (
) : !editorStore.changeDetectionState.initState.hasSucceeded || editorStore.changeDetectionState.workspaceBaseRevisionState .isBuildingEntityHashesIndex ? (
) : conflictResolutionChanges ? (
{conflictResolutionChangesDisplayLabel}
) : (
); // review changes const reviewChanges = editorStore.changeDetectionState.aggregatedWorkspaceChanges.length; const reviewChangesIndicatorStatusIcon = !reviewChanges ? (
) : !editorStore.changeDetectionState.initState.hasSucceeded || editorStore.changeDetectionState.workspaceBaseRevisionState .isBuildingEntityHashesIndex || editorStore.changeDetectionState.workspaceLocalLatestRevisionState .isBuildingEntityHashesIndex ? (
) : (
); // project latest changes const workspaceUpdateChanges = editorStore.changeDetectionState.aggregatedProjectLatestChanges.length; const workspaceUpdatePotentialConflicts = editorStore.changeDetectionState.potentialWorkspaceUpdateConflicts.length; const projectLatestChangesIndicatorStatusIcon = !workspaceUpdateChanges ? (
) : !editorStore.changeDetectionState.initState.hasSucceeded || editorStore.changeDetectionState.workspaceBaseRevisionState .isBuildingEntityHashesIndex || editorStore.changeDetectionState.projectLatestRevisionState .isBuildingEntityHashesIndex ? (
) : (
); const lazyTextModeEnabled = editorStore.mode === EDITOR_MODE.LAZY_TEXT_EDITOR; const extraActivities: ActivityBarItemConfig[] = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraActivityBarItemConfigurations?.() ?? []) .map((config) => ({ mode: config.key, title: config.title, icon: config.icon, disabled: config.disabled?.(editorStore) ?? false, })) .filter((activity) => !activity.disabled); // tabs const activities: ActivityBarItemConfig[] = [ { mode: ACTIVITY_MODE.EXPLORER, title: 'Explorer (Ctrl + Shift + X)', icon: , }, { mode: ACTIVITY_MODE.TEST_RUNNER, title: 'Test Runner', icon: , disabled: editorStore.isInConflictResolutionMode || lazyTextModeEnabled, }, { mode: ACTIVITY_MODE.LOCAL_CHANGES, title: `Local Changes (Ctrl + Shift + G)${ localChanges ? ` - ${localChanges} unpushed changes` : '' }`, icon: (
{localChangesIndicatorStatusIcon}
), disabled: editorStore.isInConflictResolutionMode, }, { mode: ACTIVITY_MODE.WORKSPACE_UPDATER, title: `Update Workspace (Ctrl + Shift + U)${ workspaceUpdateChanges ? ` - Update available${ workspaceUpdatePotentialConflicts ? ' with potential conflicts' : '' }` : '' }`, icon: (
{projectLatestChangesIndicatorStatusIcon}
), disabled: editorStore.isInConflictResolutionMode, }, { mode: ACTIVITY_MODE.WORKSPACE_REVIEW, title: `Review (Ctrl + Shift + M)${ reviewChanges ? ` - ${reviewChanges} changes` : '' }`, icon: (
{reviewChangesIndicatorStatusIcon}
), disabled: editorStore.isInConflictResolutionMode, }, { mode: ACTIVITY_MODE.CONFLICT_RESOLUTION, title: `Conflict Resolution${ conflictResolutionChanges ? ` - ${conflictResolutionChanges} changes${ conflictResolutionConflicts ? ` (${conflictResolutionConflicts} unresolved conflicts)` : '' }` : '' }`, icon: (
{conflictResolutionChangesIndicatorStatusIcon}
), disabled: !editorStore.isInConflictResolutionMode || lazyTextModeEnabled, }, { mode: ACTIVITY_MODE.PROJECT_OVERVIEW, title: 'Project', icon: , disabled: editorStore.isInConflictResolutionMode, }, { mode: ACTIVITY_MODE.WORKFLOW_MANAGER, title: 'Workflow Manager', icon: , disabled: editorStore.isInConflictResolutionMode, }, { mode: ACTIVITY_MODE.DEV_MODE, title: 'Dev Mode (Beta)', icon: ( <> ), disabled: editorStore.isInConflictResolutionMode || lazyTextModeEnabled, }, { mode: ACTIVITY_MODE.REGISTER_SERVICES, title: 'Register Service (Beta)', icon: ( <> ), disabled: editorStore.isInConflictResolutionMode || lazyTextModeEnabled, }, ].filter((activity) => !activity.disabled); const userJourneys: ActivityBarItemConfig[] = [ { mode: USER_JOURNEYS.END_TO_END_WORKFLOWS, title: 'End to End Workflows (Beta)', icon: (
), disabled: editorStore.isInConflictResolutionMode || !editorStore.applicationStore.config.options.NonProductionFeatureFlag, }, ].filter((activity) => !activity.disabled); const openShowcasePanel = () => { toggleShowcasePanel(editorStore); }; return (
{activities.map((activity) => ( ))} {Boolean(userJourneys.length) && } {userJourneys.map((activity) => ( ))} {Boolean(extraActivities.length) && } {extraActivities.map((activity) => ( ))}
} menuProps={{ anchorOrigin: { vertical: 'bottom', horizontal: 'right' }, transformOrigin: { vertical: 'bottom', horizontal: 'left' }, elevation: 7, }} >
); });