diff --git a/packages/components/src/AppInfo/index.tsx b/packages/components/src/AppInfo/index.tsx index 098ab46e8..e44120b70 100644 --- a/packages/components/src/AppInfo/index.tsx +++ b/packages/components/src/AppInfo/index.tsx @@ -18,7 +18,6 @@ export const INFO_WINDOW_WIDTH = 260; const useStyles = makeStyles((theme: Theme) => ({ infoIcon: { cursor: 'pointer', - marginLeft: theme.spacing(1), backgroundColor: 'initial', }, closeButton: { diff --git a/packages/console/src/basics/ExternalConfigurationProvider/ExternalConfigurationProvider.tsx b/packages/console/src/basics/ExternalConfigurationProvider/ExternalConfigurationProvider.tsx index 26219089f..8e1c0b07d 100644 --- a/packages/console/src/basics/ExternalConfigurationProvider/ExternalConfigurationProvider.tsx +++ b/packages/console/src/basics/ExternalConfigurationProvider/ExternalConfigurationProvider.tsx @@ -4,8 +4,9 @@ import { AppConfig } from '@flyteorg/common'; export interface ExternalConfigurationProviderProps { registry?: { nav?: React.FC; + topLevelLayout?: React.FC; taskExecutionAttemps?: React.FC; - additionalRoutes?: any; + additionalRoutes?: any[]; }; env?: any; config?: AppConfig; diff --git a/packages/console/src/basics/FeatureFlags/defaultConfig.ts b/packages/console/src/basics/FeatureFlags/defaultConfig.ts index bc67ff116..05aa3dbf8 100644 --- a/packages/console/src/basics/FeatureFlags/defaultConfig.ts +++ b/packages/console/src/basics/FeatureFlags/defaultConfig.ts @@ -10,6 +10,9 @@ export enum FeatureFlag { // Production flags LaunchPlan = 'launch-plan', + // Makes the header inline with the content + HorizontalLayout = 'horizontal-layout', + // Test Only Mine flag OnlyMine = 'only-mine', } @@ -24,6 +27,8 @@ export const defaultFlagConfig: FeatureFlagConfig = { // If you need to turn it on locally -> update runtimeConfig in ./index.tsx file 'launch-plan': false, + 'horizontal-layout': false, + 'only-mine': false, }; diff --git a/packages/console/src/components/App/App.tsx b/packages/console/src/components/App/App.tsx index 9997236c0..4f2e28a0d 100644 --- a/packages/console/src/components/App/App.tsx +++ b/packages/console/src/components/App/App.tsx @@ -34,7 +34,11 @@ import { ExternalConfigurationProvider, ExternalConfigurationProviderProps, } from 'basics/ExternalConfigurationProvider'; +import TopLevelLayoutProvider from 'components/Navigation/TopLevelLayoutState'; +import TopLevelLayout from 'components/Navigation/TopLevelLayout'; import NavBar from 'components/Navigation/NavBar'; +import { SideNavigation } from 'components/Navigation/SideNavigation'; +import GlobalStyles from 'components/utils/GlobalStyles'; export type AppComponentProps = ExternalConfigurationProviderProps; @@ -55,8 +59,12 @@ export const AppComponent: React.FC = ( } const apiState = useAPIState(); + const horizontalLayoutFlag = + `${env.HORIZONTAL_LAYOUT}`.trim().toLowerCase() === 'true'; + return ( + = ( - - + + } + sideNavigationComponent={} + routerView={} + isHorizontalLayout={horizontalLayoutFlag} + /> + @@ -91,7 +105,10 @@ export const AppComponent: React.FC = ( - + diff --git a/packages/console/src/components/Entities/EntityDetails.tsx b/packages/console/src/components/Entities/EntityDetails.tsx index 283f13143..b509dcec1 100644 --- a/packages/console/src/components/Entities/EntityDetails.tsx +++ b/packages/console/src/components/Entities/EntityDetails.tsx @@ -1,11 +1,12 @@ +import * as React from 'react'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { contentMarginGridUnits } from 'common/layout'; -import { WaitForData } from 'components/common/WaitForData'; import { EntityDescription } from 'components/Entities/EntityDescription'; import { useProject } from 'components/hooks/useProjects'; import { useChartState } from 'components/hooks/useChartState'; import { ResourceIdentifier } from 'models/Common/types'; -import * as React from 'react'; +import { Grid } from '@material-ui/core'; +import { LoadingSpinner } from 'components/common'; import { entitySections } from './constants'; import { EntityDetailsHeader } from './EntityDetailsHeader'; import { EntityInputs } from './EntityInputs'; @@ -15,6 +16,10 @@ import { EntityVersions } from './EntityVersions'; import { EntityExecutionsBarChart } from './EntityExecutionsBarChart'; const useStyles = makeStyles((theme: Theme) => ({ + entityDetailsWrapper: { + px: theme.spacing(2), + minHeight: '100vh', + }, metadataContainer: { display: 'flex', marginBottom: theme.spacing(2), @@ -57,58 +62,63 @@ interface EntityDetailsProps { */ export const EntityDetails: React.FC = ({ id }) => { const sections = entitySections[id.resourceType]; - const project = useProject(id.project); + const [project] = useProject(id.project); const styles = useStyles(); const { chartIds, onToggle, clearCharts } = useChartState(); return ( - - + + {!project?.id && } + {project?.id && ( + <> + -
- {sections.description ? ( -
- -
- ) : null} - {!sections.inputs && sections.schedules ? ( -
- +
+ {sections.description && ( +
+ +
+ )} + {!sections.inputs && sections.schedules && ( +
+ +
+ )}
- ) : null} -
- - {sections.inputs ? ( -
- -
- ) : null} - {sections.versions ? ( -
- -
- ) : null} + {!!sections.inputs && ( +
+ +
+ )} - + {!!sections.versions && ( +
+ +
+ )} - {sections.executions ? ( -
- -
- ) : null} - + {!sections.executions && } + {sections.executions && ( +
+ +
+ )} + + )} + ); }; diff --git a/packages/console/src/components/Entities/EntityDetailsHeader.tsx b/packages/console/src/components/Entities/EntityDetailsHeader.tsx index 562c46b46..eb1b9e0a6 100644 --- a/packages/console/src/components/Entities/EntityDetailsHeader.tsx +++ b/packages/console/src/components/Entities/EntityDetailsHeader.tsx @@ -72,8 +72,8 @@ export const EntityDetailsHeader: React.FC = ({ // Close modal on escape key press useEscapeKey(onCancelLaunch); - const domain = getProjectDomain(project, id.domain); - const headerText = `${domain.name} / ${id.name}`; + const domain = project ? getProjectDomain(project, id.domain) : undefined; + const headerText = domain ? `${domain.name} / ${id.name}` : ''; return ( <> diff --git a/packages/console/src/components/Entities/VersionDetails/EntityVersionDetailsContainer.tsx b/packages/console/src/components/Entities/VersionDetails/EntityVersionDetailsContainer.tsx index 4a64c2df5..f02f4d256 100644 --- a/packages/console/src/components/Entities/VersionDetails/EntityVersionDetailsContainer.tsx +++ b/packages/console/src/components/Entities/VersionDetails/EntityVersionDetailsContainer.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { withRouteParams } from 'components/common/withRouteParams'; import { ResourceIdentifier, ResourceType } from 'models/Common/types'; import { makeStyles, Theme } from '@material-ui/core/styles'; -import { WaitForData } from 'components/common/WaitForData'; import { useProject } from 'components/hooks/useProjects'; import { StaticGraphContainer } from 'components/Workflow/StaticGraphContainer'; import { WorkflowId } from 'models/Workflow/types'; @@ -10,6 +9,8 @@ import { entitySections } from 'components/Entities/constants'; import { EntityDetailsHeader } from 'components/Entities/EntityDetailsHeader'; import { EntityVersions } from 'components/Entities/EntityVersions'; import { RouteComponentProps } from 'react-router-dom'; +import { Box } from '@material-ui/core'; +import { LoadingSpinner } from 'components/common'; import { typeNameToEntityResource } from '../constants'; import { versionsDetailsSections } from './constants'; import { EntityVersionDetails } from './EntityVersionDetails'; @@ -26,6 +27,7 @@ const useStyles = makeStyles((theme: Theme) => ({ flexWrap: 'nowrap', overflow: 'hidden', height: `calc(100vh - ${theme.spacing(17)}px)`, + padding: theme.spacing(0, 2), }, staticGraphContainer: { display: 'flex', @@ -40,10 +42,12 @@ const useStyles = makeStyles((theme: Theme) => ({ width: '100%', flex: '1', overflowY: 'scroll', + padding: theme.spacing(0, 2), }, versionsContainer: { display: 'flex', flex: '0 1 auto', + padding: theme.spacing(0, 2), height: ({ resourceType }) => resourceType === ResourceType.LAUNCH_PLAN ? '100%' : '40%', flexDirection: 'column', @@ -82,17 +86,23 @@ const EntityVersionsDetailsContainerImpl: React.FC< const id = workflowId as ResourceIdentifier; const sections = entitySections[id.resourceType]; const versionsSections = versionsDetailsSections[id.resourceType]; - const project = useProject(workflowId.project); + const [project] = useProject(workflowId.project); const styles = useStyles({ resourceType: id.resourceType }); + if (!project?.id) { + return ; + } + return ( - - + <> + + +
{versionsSections.details && (
@@ -108,7 +118,7 @@ const EntityVersionsDetailsContainerImpl: React.FC<
-
+ ); }; diff --git a/packages/console/src/components/Entities/test/EntityDetails.test.tsx b/packages/console/src/components/Entities/test/EntityDetails.test.tsx index 8189026e1..f78b2e9fc 100644 --- a/packages/console/src/components/Entities/test/EntityDetails.test.tsx +++ b/packages/console/src/components/Entities/test/EntityDetails.test.tsx @@ -8,8 +8,11 @@ import { Workflow } from 'models/Workflow/types'; import { projects } from 'mocks/data/projects'; import * as projectApi from 'models/Project/api'; import { MemoryRouter } from 'react-router'; +import { QueryClient, QueryClientProvider } from 'react-query'; import { EntityDetails } from '../EntityDetails'; +const queryClient = new QueryClient(); + jest.mock('models/Project/api'); describe('EntityDetails', () => { @@ -27,9 +30,11 @@ describe('EntityDetails', () => { const renderDetails = (id: ResourceIdentifier) => { return render( - - - , + + + + + , ); }; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadata.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadata.tsx index 651cb5625..84c8f214c 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadata.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadata.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Typography } from '@material-ui/core'; +import { Grid, Typography } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import classnames from 'classnames'; import { dashedValueString } from 'common/constants'; @@ -18,19 +18,15 @@ const useStyles = makeStyles((theme: Theme) => { return { container: { background: secondaryBackgroundColor, - display: 'flex', - flexDirection: 'column', - position: 'relative', + width: '100%', }, detailsContainer: { - alignItems: 'start', display: 'flex', - flex: '0 1 auto', - paddingTop: theme.spacing(3), + paddingTop: theme.spacing(1), paddingBottom: theme.spacing(2), + marginTop: 0, }, detailItem: { - flexShrink: 0, marginLeft: theme.spacing(4), }, expandCollapseButton: { @@ -111,9 +107,13 @@ export const ExecutionMetadata: React.FC<{}> = () => { return (
-
+ {details.map(({ className, label, value }) => ( -
+ = () => { > {value} -
+
))} -
+ {error || abortMetadata ? ( {details.map(({ className, label, value }) => ( -
+ {label} @@ -102,7 +106,7 @@ export const ExecutionMetadataExtra: React.FC<{ > {value} -
+ ))} ); diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TimelineChart/utils.ts b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TimelineChart/utils.ts index 388b009a4..b723fdfff 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TimelineChart/utils.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TimelineChart/utils.ts @@ -176,7 +176,7 @@ export const getExecutionMetricsOperationIds = ( const operationIds = uniq( traverse(data) .paths() - .filter(path => path.at(-1) === 'operationId') + .filter(path => path[path.length - 1] === 'operationId') .map(path => get(data, path)), ); diff --git a/packages/console/src/components/Executions/ExecutionFilters.tsx b/packages/console/src/components/Executions/ExecutionFilters.tsx index a2ae877fe..43b494117 100644 --- a/packages/console/src/components/Executions/ExecutionFilters.tsx +++ b/packages/console/src/components/Executions/ExecutionFilters.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { FormControlLabel, Checkbox, FormGroup } from '@material-ui/core'; +import { FormControlLabel, Checkbox, FormGroup, Grid } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { MultiSelectForm } from 'components/common/MultiSelectForm'; import { SearchInputForm } from 'components/common/SearchInputForm'; @@ -18,9 +18,10 @@ const useStyles = makeStyles((theme: Theme) => ({ alignItems: 'center', display: 'flex', flexDirection: 'row', - height: theme.spacing(7), minHeight: theme.spacing(7), paddingLeft: theme.spacing(1), + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(1), width: '100%', }, filterButton: { @@ -95,7 +96,7 @@ export const ExecutionFilters: React.FC = ({ }); return ( -
+ {filters.map((filter: any) => { if (filter.hidden) { return null; @@ -105,27 +106,31 @@ export const ExecutionFilters: React.FC = ({ filter.setActive(event.target.checked); return ( - - } - className={styles.checkbox} - label={filter.label} - /> + + + } + className={styles.checkbox} + label={filter.label} + /> + ); } return ( - } - /> + + } + /> + ); })} {chartIds && chartIds.length > 0 && ( @@ -141,38 +146,42 @@ export const ExecutionFilters: React.FC = ({ /> )} {!!onlyMyExecutionsFilterState && ( - - - onlyMyExecutionsFilterState.onOnlyMyExecutionsFilterChange( - checked, - ) - } - /> - } - className={styles.checkbox} - label="Only my executions" - /> - + + + + onlyMyExecutionsFilterState.onOnlyMyExecutionsFilterChange( + checked, + ) + } + /> + } + className={styles.checkbox} + label="Only my executions" + /> + + )} {!!onArchiveFilterChange && ( - - onArchiveFilterChange(checked)} - /> - } - className={styles.checkbox} - label="Show archived executions" - /> - + + + onArchiveFilterChange(checked)} + /> + } + className={styles.checkbox} + label="Show archived executions" + /> + + )} -
+ ); }; diff --git a/packages/console/src/components/Executions/Tables/styles.ts b/packages/console/src/components/Executions/Tables/styles.ts index 19543adbd..b76fa4f79 100644 --- a/packages/console/src/components/Executions/Tables/styles.ts +++ b/packages/console/src/components/Executions/Tables/styles.ts @@ -91,6 +91,7 @@ export const useExecutionTableStyles = makeStyles((theme: Theme) => ({ minWidth: 0, paddingBottom: theme.spacing(1), paddingTop: theme.spacing(1), + overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }, diff --git a/packages/console/src/components/Launch/LaunchForm/styles.ts b/packages/console/src/components/Launch/LaunchForm/styles.ts index 900c73520..f3318018e 100644 --- a/packages/console/src/components/Launch/LaunchForm/styles.ts +++ b/packages/console/src/components/Launch/LaunchForm/styles.ts @@ -57,6 +57,6 @@ export const useStyles = makeStyles((theme: Theme) => ({ }, }, collapsibleSection: { - margin: '0 -16px', + margin: 0, }, })); diff --git a/packages/console/src/components/Navigation/DefaultAppBarContent.tsx b/packages/console/src/components/Navigation/DefaultAppBarContent.tsx index 452a73460..d7b98ca58 100644 --- a/packages/console/src/components/Navigation/DefaultAppBarContent.tsx +++ b/packages/console/src/components/Navigation/DefaultAppBarContent.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { makeStyles, Theme } from '@material-ui/core/styles'; +import { makeStyles, useTheme } from '@material-ui/core/styles'; import classnames from 'classnames'; import { AppInfo, VersionInfo } from '@flyteorg/components'; import { FlyteLogo } from '@flyteorg/ui-atoms'; @@ -9,20 +9,15 @@ import { Routes } from 'routes/routes'; import { FeatureFlag, useFeatureFlag } from 'basics/FeatureFlags'; import { useAdminVersion } from 'components/hooks/useVersion'; import { env } from '@flyteorg/common'; +import { Box, Grid, IconButton } from '@material-ui/core'; +import MenuIcon from '@material-ui/icons/Menu'; +import debounce from 'lodash/debounce'; import { NavigationDropdown } from './NavigationDropdown'; import { UserInformation } from './UserInformation'; import { OnlyMine } from './OnlyMine'; import { FlyteNavItem } from './utils'; import t, { patternKey } from './strings'; - -const useStyles = makeStyles((theme: Theme) => ({ - spacer: { - flexGrow: 1, - }, - rightNavBarItem: { - marginLeft: theme.spacing(2), - }, -})); +import { TopLevelLayoutContext } from './TopLevelLayoutState'; interface DefaultAppBarProps { items: FlyteNavItem[]; @@ -33,13 +28,47 @@ interface DefaultAppBarProps { export const DefaultAppBarContent = (props: DefaultAppBarProps) => { const [platformVersion, setPlatformVersion] = React.useState(''); const [consoleVersion, setConsoleVersion] = React.useState(''); + const { + isMobileNav, + openSideNav, + closeSideNav, + isSideNavOpen, + isLayoutHorizontal, + showMobileNav, + hideMobileNav, + } = React.useContext(TopLevelLayoutContext); + const commonStyles = useCommonStyles(); - const styles = useStyles(); const isFlagEnabled = useFeatureFlag(FeatureFlag.OnlyMine); const { adminVersion } = useAdminVersion(); const isGAEnabled = env.ENABLE_GA === 'true' && env.GA_TRACKING_ID !== ''; + const handleSideNavToggle = React.useCallback(() => { + return isSideNavOpen ? closeSideNav() : openSideNav(); + }, [isSideNavOpen, openSideNav, closeSideNav]); + + const theme = useTheme(); + + // Enable / Disable mobile nav behaviour based on screen size + React.useLayoutEffect(() => { + const handleResize = () => { + if (window.innerWidth < theme.breakpoints.values.md) { + if (!isMobileNav) { + showMobileNav(); + closeSideNav(); + } + } else if (isMobileNav) { + hideMobileNav(); + closeSideNav(); + } + }; + handleResize(); + const debouncedResize = debounce(handleResize, 50); + window.addEventListener('resize', debouncedResize); + return () => window.removeEventListener('resize', debouncedResize); + }, [closeSideNav, theme.breakpoints.values.md]); + React.useEffect(() => { try { const { version } = require('../../../../../website/package.json'); @@ -74,27 +103,101 @@ export const DefaultAppBarContent = (props: DefaultAppBarProps) => { }, ]; + const styles = makeStyles(() => ({ + wordmark: { + position: 'relative', + paddingTop: theme.spacing(2.75), + '& > svg': { + height: '22px', + transform: 'translateX(-34px)', + marginTop: '4px', + top: '0', + position: 'absolute', + }, + '& > svg > path:first-child': { + display: 'none', + }, + }, + flex: { + display: 'flex', + }, + }))(); + return ( - <> - - - - {props.items?.length > 0 ? ( - - ) : ( - false - )} -
- {isFlagEnabled && } - - - + + + + {isMobileNav && ( + + + menu + + + )} + + + + {isLayoutHorizontal && ( + + + + )} + + {props.items?.length > 0 && ( + + )} + + + + + + {isFlagEnabled && ( + + + + )} + + + + + + + + + + + ); }; diff --git a/packages/console/src/components/Navigation/NavBar.tsx b/packages/console/src/components/Navigation/NavBar.tsx index 0946d489f..8377cbb5a 100644 --- a/packages/console/src/components/Navigation/NavBar.tsx +++ b/packages/console/src/components/Navigation/NavBar.tsx @@ -5,7 +5,10 @@ import Toolbar from '@material-ui/core/Toolbar'; import { navBarContentId } from 'common/constants'; import { FlyteNavigation } from '@flyteorg/common'; import { useExternalConfigurationContext } from 'basics/ExternalConfigurationProvider'; +import { makeStyles } from '@material-ui/core'; +import { CSSProperties } from '@material-ui/core/styles/withStyles'; import { getFlyteNavigationData } from './utils'; +import { useTopLevelLayoutContext } from './TopLevelLayoutState'; export interface NavBarProps { useCustomContent?: boolean; @@ -17,6 +20,32 @@ const DefaultAppBarContent = lazy(() => import('./DefaultAppBarContent')); /** Contains all content in the top navbar of the application. */ export const NavBar = (props: NavBarProps) => { const navData = props.navigationData ?? getFlyteNavigationData(); + const layoutState = useTopLevelLayoutContext(); + + const styles = makeStyles(theme => ({ + stackedSpacer: theme.mixins.toolbar as CSSProperties, + horizontalSpacer: { + width: '80px', + }, + navBar: { + color: navData?.color, + background: navData?.background, + top: 0, + }, + inlineNavBar: { + width: '80px', + height: '100%', + position: 'fixed', + inset: '0', + }, + inlineToolBar: { + padding: theme.spacing(2, 0, 4, 0), + height: '100%', + }, + }))(); + + const { isLayoutHorizontal } = layoutState; + const navBarContent = props.useCustomContent ? (
) : ( @@ -33,20 +62,31 @@ export const NavBar = (props: NavBarProps) => { const ExternalNav = registry?.nav; return ExternalNav ? ( - + ) : ( - - {navBarContent} - + <> + {isLayoutHorizontal ? ( +
+ ) : ( +
+ )} + + + {navBarContent} + + + ); }; diff --git a/packages/console/src/components/Navigation/ProjectNavigation.tsx b/packages/console/src/components/Navigation/ProjectNavigation.tsx index ab8c147b6..5269c8f59 100644 --- a/packages/console/src/components/Navigation/ProjectNavigation.tsx +++ b/packages/console/src/components/Navigation/ProjectNavigation.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { makeStyles, Theme } from '@material-ui/core/styles'; +import { makeStyles, Theme, useTheme } from '@material-ui/core/styles'; import { SvgIconProps } from '@material-ui/core/SvgIcon'; import ChevronRight from '@material-ui/icons/ChevronRight'; import DeviceHub from '@material-ui/icons/DeviceHub'; @@ -21,6 +21,7 @@ import { import { primaryHighlightColor } from 'components/Theme/constants'; import { ProjectSelector } from './ProjectSelector'; import NavLinkWithSearch from './NavLinkWithSearch'; +import { TopLevelLayoutContext } from './TopLevelLayoutState'; interface ProjectNavigationRouteParams { domainId?: string; @@ -72,8 +73,11 @@ const ProjectNavigationImpl: React.FC = ({ }) => { const styles = useStyles(); const commonStyles = useCommonStyles(); - const project = useProject(projectId); - const projects = useProjects(); + if (!projectId) return no project id; + + const [projects] = useProjects(); + const [project] = useProject(projectId); + const onProjectSelected = (project: Project) => { const path = Routes.ProjectDetails.makeUrl(project.id, section); const projectDomain = { @@ -85,83 +89,92 @@ const ProjectNavigationImpl: React.FC = ({ return history.push(path); }; - const routes: ProjectRoute[] = [ - { - icon: Dashboard, - isActive: (match, location) => { - const finalMatch = match - ? match - : matchPath(location.pathname, { - path: Routes.ProjectDashboard.path, - exact: false, - }); - return !!finalMatch; + const routes: ProjectRoute[] = React.useMemo(() => { + if (!project?.id && !domainId) return []; + return [ + { + icon: Dashboard, + isActive: (match, location) => { + const finalMatch = match + ? match + : matchPath(location.pathname, { + path: Routes.ProjectDashboard.path, + exact: false, + }); + return !!finalMatch; + }, + path: Routes.ProjectDetails.sections.dashboard.makeUrl( + project.id, + domainId, + ), + text: 'Project Dashboard', }, - path: Routes.ProjectDetails.sections.dashboard.makeUrl( - project.value.id, - domainId, - ), - text: 'Project Dashboard', - }, - { - icon: DeviceHub, - isActive: (match, location) => { - const finalMatch = match - ? match - : matchPath(location.pathname, { - path: Routes.WorkflowDetails.path, - exact: false, - }); - return !!finalMatch; + { + icon: DeviceHub, + isActive: (match, location) => { + const finalMatch = match + ? match + : matchPath(location.pathname, { + path: Routes.WorkflowDetails.path, + exact: false, + }); + return !!finalMatch; + }, + path: Routes.ProjectDetails.sections.workflows.makeUrl( + projectId, + domainId, + ), + text: 'Workflows', }, - path: Routes.ProjectDetails.sections.workflows.makeUrl( - project.value.id, - domainId, - ), - text: 'Workflows', - }, - { - icon: LinearScale, - isActive: (match, location) => { - const finalMatch = match - ? match - : matchPath(location.pathname, { - path: Routes.TaskDetails.path, - exact: false, - }); - return !!finalMatch; + { + icon: LinearScale, + isActive: (match, location) => { + const finalMatch = match + ? match + : matchPath(location.pathname, { + path: Routes.TaskDetails.path, + exact: false, + }); + return !!finalMatch; + }, + path: Routes.ProjectDetails.sections.tasks.makeUrl(projectId, domainId), + text: 'Tasks', }, - path: Routes.ProjectDetails.sections.tasks.makeUrl( - project.value.id, - domainId, - ), - text: 'Tasks', - }, - { - icon: MuiLaunchPlanIcon, - isActive: (match, location) => { - const finalMatch = match - ? match - : matchPath(location.pathname, { - path: Routes.LaunchPlanDetails.path, - exact: false, - }); - return !!finalMatch; + { + icon: MuiLaunchPlanIcon as any, + isActive: (match, location) => { + const finalMatch = match + ? match + : matchPath(location.pathname, { + path: Routes.LaunchPlanDetails.path, + exact: false, + }); + return !!finalMatch; + }, + path: Routes.ProjectDetails.sections.launchPlans.makeUrl( + project.id, + domainId, + ), + text: 'Launch Plans', }, - path: Routes.ProjectDetails.sections.launchPlans.makeUrl( - project.value.id, - domainId, - ), - text: 'Launch Plans', - }, - ]; + ]; + }, [project?.id, domainId]); + + const { openSideNav } = React.useContext(TopLevelLayoutContext); + const theme = useTheme(); + React.useEffect(() => { + if (window.innerWidth > theme.breakpoints.values.md) { + openSideNav(); + } + }, []); + if (!project && !projects) return <>; return ( <> - {project.value && projects.value && ( + {project?.id && ( )} diff --git a/packages/console/src/components/Navigation/SideNavigation.tsx b/packages/console/src/components/Navigation/SideNavigation.tsx index 4f2af0903..ecfdcdc34 100644 --- a/packages/console/src/components/Navigation/SideNavigation.tsx +++ b/packages/console/src/components/Navigation/SideNavigation.tsx @@ -1,21 +1,34 @@ +import * as React from 'react'; import { makeStyles, Theme } from '@material-ui/core/styles'; -import { navbarGridHeight, sideNavGridWidth } from 'common/layout'; +import { sideNavGridWidth } from 'common/layout'; import { separatorColor } from 'components/Theme/constants'; -import * as React from 'react'; import { Route } from 'react-router-dom'; import { projectBasePath } from 'routes/constants'; import { ProjectNavigation } from './ProjectNavigation'; const useStyles = makeStyles((theme: Theme) => ({ - root: { - borderRight: `1px solid ${separatorColor}`, - display: 'flex', - flexDirection: 'column', - bottom: 0, + wrapper: { + position: 'relative', + height: '100%', + width: theme.spacing(sideNavGridWidth), + }, + absolute: { + position: 'relative', + top: 0, left: 0, + bottom: 0, + width: '100%', + height: '100%', + }, + fixed: { position: 'fixed', - top: theme.spacing(navbarGridHeight), + top: 0, + height: 'calc(100dvh - 64px)', width: theme.spacing(sideNavGridWidth), + transition: 'top 0s', + }, + border: { + borderRight: `1px solid ${separatorColor}`, }, })); @@ -23,11 +36,17 @@ const useStyles = makeStyles((theme: Theme) => ({ export const SideNavigation: React.FC = () => { const styles = useStyles(); return ( -
- +
+
+
+ +
+
); }; diff --git a/packages/console/src/components/Navigation/TopLevelLayout.tsx b/packages/console/src/components/Navigation/TopLevelLayout.tsx new file mode 100644 index 000000000..b7486167a --- /dev/null +++ b/packages/console/src/components/Navigation/TopLevelLayout.tsx @@ -0,0 +1,285 @@ +import React, { useEffect, useLayoutEffect, useMemo, useRef } from 'react'; +import { + Grid, + styled, + makeStyles, + Box, + useTheme, + Toolbar, +} from '@material-ui/core'; +import { ContentContainer } from 'components/common/ContentContainer'; +import { useExternalConfigurationContext } from 'basics/ExternalConfigurationProvider'; +import { sideNavGridWidth } from 'common/layout'; +import debounce from 'lodash/debounce'; +import { FeatureFlag, useFeatureFlagContext } from 'basics/FeatureFlags'; +import { subnavBackgroundColor } from 'components/Theme/constants'; +import { subnavBarContentId } from 'common/constants'; +import { TopLevelLayoutContext } from './TopLevelLayoutState'; + +const StyledSubNavBarContent = styled(Toolbar)(() => ({ + minHeight: 'auto', + padding: 0, + margin: 0, + + '& > *': { + alignItems: 'center', + display: 'flex', + maxWidth: '100%', + padding: '24px 20px 24px 30px', + background: subnavBackgroundColor, + }, + '@media (min-width: 600px)': { + minHeight: 'auto', + }, +})); + +const GrowGrid = styled(Grid)(() => ({ + display: 'flex', + flexGrow: 1, +})); + +export interface TopLevelLayoutInterFace { + headerComponent: JSX.Element; + sideNavigationComponent: JSX.Element; + routerView: JSX.Element; + className?: string; + isHorizontalLayout: boolean; +} + +export const TopLevelLayoutGrid = ({ + headerComponent, + sideNavigationComponent, + routerView, + className = '', + isHorizontalLayout = false, +}: TopLevelLayoutInterFace) => { + const userHorizontalPref = isHorizontalLayout; + const theme = useTheme(); + const HeaderComponent = headerComponent ? () => headerComponent : () => <>; + const SideNavigationComponent = sideNavigationComponent + ? () => sideNavigationComponent + : () => <>; + const RouterView = routerView ? () => routerView : () => <>; + + const styles = makeStyles(theme => ({ + noBounce: { + '-webkit-overflow-scrolling': + 'touch' /* enables “momentum” (smooth) scrolling */, + }, + sticky: { + position: 'sticky', + top: 0, + }, + relative: { + position: 'relative', + }, + absolute: { + position: 'absolute', + }, + w100: { + width: '100%', + }, + h100: { + minHeight: '100dvh', + }, + headerZIndex: { + zIndex: 2, + }, + leftNavZIndex: { + zIndex: 1, + }, + above: { + zIndex: 1, + }, + nav: { + top: 0, + position: 'relative', + height: '100%', + minWidth: theme.spacing(sideNavGridWidth), + background: theme.palette.background.paper, + willChange: 'transform', + }, + mobileNav: { + zIndex: 2, + position: 'absolute', + boxShadow: theme.shadows[4], + }, + sideNavAnimation: { + animationName: `$sideNavAnimation`, + animationTimingFunction: `${theme.transitions.easing.easeInOut}`, + animationDuration: `300ms`, + animationFillMode: 'forwards', + }, + '@keyframes sideNavAnimation': { + '0%': { + opacity: 0, + display: 'none', + }, + '1%': { + opacity: 0, + transform: 'translateX(-20%)', + display: 'block', + }, + '99%': { + opacity: 1, + transform: 'translateX(0)', + }, + '100%': { + opacity: 1, + display: 'block', + }, + }, + closeSideNav: { + animationDirection: 'reverse', + display: 'none', + }, + openSideNav: { + animationDirection: 'normal', + }, + }))(); + + const { + isMobileNav, + isSideNavOpen, + closeSideNav, + isLayoutHorizontal, + rowLayout, + columnLayout, + } = React.useContext(TopLevelLayoutContext); + + // flip layout on narrow screen per flag and resizes + useLayoutEffect(() => { + const handleResize = () => { + if (window.innerWidth < theme.breakpoints.values.md) { + rowLayout(); + } else { + if (!userHorizontalPref) { + rowLayout(); + } else { + columnLayout(); + } + } + }; + + handleResize(); + const debouncedResize = debounce(handleResize, 50); + window.addEventListener('resize', debouncedResize); + return () => window.removeEventListener('resize', debouncedResize); + }, []); + + // run on init + useEffect(() => { + if (isMobileNav || !isLayoutHorizontal || !userHorizontalPref) { + rowLayout(); + closeSideNav(); + } else { + columnLayout(); + } + }, []); + + // ref to update offset on scroll + const scrollRef = useRef(null); + // pin left nav to top of screen + useLayoutEffect(() => { + const handleScroll = () => { + const scrollElement = scrollRef.current; + const documentHeight = + document.body.scrollHeight - document.body.clientHeight; + + if (scrollElement && window.scrollY + 1 < documentHeight) { + const scroll = window.scrollY; + scrollElement.style.transform = `translateY(${scroll}px)`; + } + }; + + document.addEventListener('scroll', handleScroll); + return () => { + document.removeEventListener('scroll', handleScroll); + }; + }, []); + + return ( + + + + + + {/* Grow X Axis */} + + + + + + + + + {/* Legacy, need to move to */} + + + + + + + + + + + + ); +}; + +export const TopLevelLayout = (props: TopLevelLayoutInterFace) => { + const { registry } = useExternalConfigurationContext(); + const ExternalTopLevelLayout = registry?.topLevelLayout; + + const { getFeatureFlag } = useFeatureFlagContext(); + const flag = getFeatureFlag(FeatureFlag.HorizontalLayout); + + const isHorizontalLayout = useMemo( + () => flag || props.isHorizontalLayout, + [flag, props.isHorizontalLayout], + ); + + if (ExternalTopLevelLayout) + return ( + + ); + return ( + + ); +}; + +export default TopLevelLayout; diff --git a/packages/console/src/components/Navigation/TopLevelLayoutState.tsx b/packages/console/src/components/Navigation/TopLevelLayoutState.tsx new file mode 100644 index 000000000..89eb24c55 --- /dev/null +++ b/packages/console/src/components/Navigation/TopLevelLayoutState.tsx @@ -0,0 +1,94 @@ +import React, { + createContext, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; + +const isMobileInit = window.innerWidth < 960; + +const initValues = { + isSideNavOpen: false, + isMobileNav: isMobileInit, // < md breakboint + openSideNav: () => {}, + closeSideNav: () => {}, + isLayoutHorizontal: !isMobileInit, + columnLayout: () => {}, + rowLayout: () => {}, + showMobileNav: () => {}, + hideMobileNav: () => {}, +}; + +export const TopLevelLayoutContext = createContext(initValues); + +export const useTopLevelLayoutContext = () => { + return useContext(TopLevelLayoutContext); +}; + +const TopLevelLayoutProvider = ({ children }) => { + const [isMobileNav, setIsMobileNav] = useState(initValues.isMobileNav); + const [isSideNavOpen, setIsSideNavOpen] = useState(initValues.isMobileNav); + const [isLayoutHorizontal, setisLayoutHorizontal] = useState( + initValues.isLayoutHorizontal, + ); + + const openSideNav = useCallback( + () => setIsSideNavOpen(true), + [isSideNavOpen, setIsSideNavOpen], + ); + const closeSideNav = useCallback( + () => setIsSideNavOpen(false), + [isSideNavOpen, setIsSideNavOpen], + ); + + const columnLayout = useCallback( + () => setisLayoutHorizontal(true), + [isLayoutHorizontal, setisLayoutHorizontal], + ); + const rowLayout = useCallback( + () => setisLayoutHorizontal(false), + [isLayoutHorizontal, setisLayoutHorizontal], + ); + + const showMobileNav = useCallback( + () => setIsMobileNav(true), + [isMobileNav, setIsMobileNav], + ); + const hideMobileNav = useCallback( + () => setIsMobileNav(false), + [isMobileNav, setIsMobileNav], + ); + + const value = useMemo(() => { + return { + isMobileNav, + isSideNavOpen, + openSideNav, + closeSideNav, + isLayoutHorizontal, + columnLayout, + rowLayout, + showMobileNav, + hideMobileNav, + }; + }, [ + isMobileNav, + isSideNavOpen, + openSideNav, + closeSideNav, + isLayoutHorizontal, + columnLayout, + rowLayout, + showMobileNav, + hideMobileNav, + ]); + + return ( + + <>{children} + + ); +}; + +export default TopLevelLayoutProvider; diff --git a/packages/console/src/components/Navigation/UserInformation.tsx b/packages/console/src/components/Navigation/UserInformation.tsx index d06efbdbc..f82cb9abe 100644 --- a/packages/console/src/components/Navigation/UserInformation.tsx +++ b/packages/console/src/components/Navigation/UserInformation.tsx @@ -1,6 +1,15 @@ import * as React from 'react'; import { useFlyteApi } from '@flyteorg/flyte-api'; -import { Link, makeStyles, Theme } from '@material-ui/core'; +import { + Avatar, + Box, + IconButton, + Link, + makeStyles, + Popover, + Theme, + Typography, +} from '@material-ui/core'; import { WaitForData } from 'components/common/WaitForData'; import { useUserProfile } from 'components/hooks/useUserProfile'; import t from './strings'; @@ -9,34 +18,113 @@ const useStyles = makeStyles((theme: Theme) => ({ container: { color: theme.palette.common.white, }, + avatar: { + width: '2rem', + height: '2rem', + fontSize: '1rem', + backgroundColor: theme.palette.secondary.main, + border: `1px solid ${theme.palette.common.white}`, + }, })); const LoginLink = (props: { loginUrl: string }) => { return ( - - {t('login')} - + + + {t('login')} + + ); }; /** Displays user info if logged in, or a login link otherwise. */ export const UserInformation: React.FC<{}> = () => { - const style = useStyles(); + const styles = useStyles(); const profile = useUserProfile(); const apiContext = useFlyteApi(); + const [anchorEl, setAnchorEl] = React.useState(null); + + const handlePopoverOpen = event => { + setAnchorEl(event.currentTarget); + }; + + const handlePopoverClose = () => { + setAnchorEl(null); + }; + + const userName = React.useMemo(() => { + if (!profile.value) { + return null; + } + + return profile.value.preferredUsername + ? profile.value.preferredUsername + : profile.value.name; + }, [profile.value]); + + const givenName = React.useMemo(() => { + if (!profile.value) { + return null; + } + + return profile.value.name + ? profile.value.name + : `${profile.value.givenName} ${profile.value.familyName}`.trim(); + }, [profile.value]); + + const userNameInitial = React.useMemo(() => { + if (!givenName) { + return ''; + } + const names = givenName.split(' '); + const firstInitial = names[0].charAt(0); + const lastInitial = + names.length > 1 ? names[names.length - 1].charAt(0) : ''; + return `${firstInitial}${lastInitial}`.toLocaleUpperCase(); + }, [givenName]); + + const open = Boolean(anchorEl); + return ( -
- {!profile.value ? ( - - ) : !profile.value.preferredUsername || - profile.value.preferredUsername === '' ? ( - profile.value.name - ) : ( - profile.value.preferredUsername - )} -
+ {!profile.value && } + {profile.value && ( + <> + + + + {userName} + + + + {userName} + + + + + )}
); }; diff --git a/packages/console/src/components/Navigation/index.ts b/packages/console/src/components/Navigation/index.ts index bd573f924..f1cf5bd57 100644 --- a/packages/console/src/components/Navigation/index.ts +++ b/packages/console/src/components/Navigation/index.ts @@ -1,3 +1,20 @@ +import TopLevelLayout, { + TopLevelLayoutGrid, + TopLevelLayoutInterFace, +} from './TopLevelLayout'; +import TopLevelLayoutProvider, { + TopLevelLayoutContext, + useTopLevelLayoutContext, +} from './TopLevelLayoutState'; + export * from './UserInformation'; export * from './NavBarContent'; export * from './SubNavBarContent'; +export { + TopLevelLayout, + TopLevelLayoutGrid, + TopLevelLayoutContext, + useTopLevelLayoutContext, + TopLevelLayoutProvider, +}; +export type { TopLevelLayoutInterFace }; diff --git a/packages/console/src/components/Project/ProjectDetails.tsx b/packages/console/src/components/Project/ProjectDetails.tsx index 1a6be1044..2afc15e49 100644 --- a/packages/console/src/components/Project/ProjectDetails.tsx +++ b/packages/console/src/components/Project/ProjectDetails.tsx @@ -1,14 +1,14 @@ +import * as React from 'react'; import { Tab, Tabs } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; -import { WaitForData } from 'components/common/WaitForData'; import { withRouteParams } from 'components/common/withRouteParams'; import { useProject } from 'components/hooks/useProjects'; import { useQueryState } from 'components/hooks/useQueryState'; import { Project } from 'models/Project/types'; -import * as React from 'react'; import { Redirect, Route, Switch } from 'react-router'; import { Routes } from 'routes/routes'; import { RouteComponentProps } from 'react-router-dom'; +import { LoadingSpinner } from 'components/common'; import { ProjectDashboard } from './ProjectDashboard'; import { ProjectTasks } from './ProjectTasks'; import { ProjectWorkflows } from './ProjectWorkflows'; @@ -41,23 +41,42 @@ const ProjectEntitiesByDomain: React.FC<{ }> = ({ entityType, project }) => { const styles = useStyles(); const { params, setQueryState } = useQueryState<{ domain: string }>(); - if (project.domains.length === 0) { + if (project && !project?.domains) { throw new Error('No domains exist for this project'); } - const domainId = params.domain || project.domains[0].id; + const domainId = React.useMemo(() => { + if (params?.domain) { + return params.domain; + } + return project?.domains ? project?.domains[0].id : ''; + }, [project, project?.domains, params?.domain]); + const handleTabChange = (_event: React.ChangeEvent, tabId: string) => setQueryState({ domain: tabId, }); const EntityComponent = entityTypeToComponent[entityType]; + return ( <> - - {project.domains.map(({ id, name }) => ( - - ))} - - + {project?.domains && ( + + {project.domains.map(({ id, name }) => ( + + ))} + + )} + {project?.id ? ( + + ) : ( + <> + + + )} ); }; @@ -82,31 +101,30 @@ const ProjectLaunchPlansByDomain: React.FC<{ project: Project }> = ({ export const ProjectDetailsContainer: React.FC = ({ projectId, }) => { - const project = useProject(projectId); + const [project] = useProject(projectId); + + if (!project?.id) { + return ; + } + return ( - - {() => { - return ( - - - - - - - - - - - - - - - - ); - }} - + + + + + + + + + + + + + + + ); }; diff --git a/packages/console/src/components/SelectProject/SelectProject.tsx b/packages/console/src/components/SelectProject/SelectProject.tsx index 77a0c2e0f..fe322cd41 100644 --- a/packages/console/src/components/SelectProject/SelectProject.tsx +++ b/packages/console/src/components/SelectProject/SelectProject.tsx @@ -1,10 +1,10 @@ +import * as React from 'react'; import { makeStyles, Theme } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import { SearchableList, SearchResult } from 'components/common/SearchableList'; -import { WaitForData } from 'components/common/WaitForData'; import { useProjects } from 'components/hooks/useProjects'; import { Project } from 'models/Project/types'; -import * as React from 'react'; +import { TopLevelLayoutContext } from 'components/Navigation/TopLevelLayoutState'; import { ProjectList } from './ProjectList'; const useStyles = makeStyles((theme: Theme) => ({ @@ -23,32 +23,39 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -const renderProjectList = (results: SearchResult[]) => ( - r.value)} /> +const renderProjectList = (projects: SearchResult[]) => ( + p.value)} /> ); /** The view component for the landing page of the application. */ export const SelectProject: React.FC = () => { const styles = useStyles(); - const projects = useProjects(); + const [projects] = useProjects(); + + const { isSideNavOpen, closeSideNav } = React.useContext( + TopLevelLayoutContext, + ); + React.useEffect(() => { + // Side nav is always closed on this page + closeSideNav(); + }, [closeSideNav, isSideNavOpen]); + return ( - -
-

Welcome to Flyte

- -

Select a project to get started...

-
-
-
- -
-
-
-
+
+

Welcome to Flyte

+ +

Select a project to get started...

+
+
+
+ +
+
+
); }; diff --git a/packages/console/src/components/Tables/PaginatedDataList.tsx b/packages/console/src/components/Tables/PaginatedDataList.tsx index 92a4b9ed0..2bf44dc10 100644 --- a/packages/console/src/components/Tables/PaginatedDataList.tsx +++ b/packages/console/src/components/Tables/PaginatedDataList.tsx @@ -48,7 +48,7 @@ const useStyles = makeStyles((theme: Theme) => marginBottom: theme.spacing(2), }, table: { - minWidth: 750, + minWidth: 200, }, radioButton: { width: workflowVersionsTableColumnWidths.radio, diff --git a/packages/console/src/components/Workflow/WorkflowVersionDetails.tsx b/packages/console/src/components/Workflow/WorkflowVersionDetails.tsx index bb632811d..670ec0f2e 100644 --- a/packages/console/src/components/Workflow/WorkflowVersionDetails.tsx +++ b/packages/console/src/components/Workflow/WorkflowVersionDetails.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { withRouteParams } from 'components/common/withRouteParams'; import { ResourceIdentifier, ResourceType } from 'models/Common/types'; import { makeStyles, Theme } from '@material-ui/core/styles'; -import { WaitForData } from 'components/common/WaitForData'; import { useProject } from 'components/hooks/useProjects'; import { StaticGraphContainer } from 'components/Workflow/StaticGraphContainer'; import { WorkflowId } from 'models/Workflow/types'; @@ -10,6 +9,7 @@ import { entitySections } from 'components/Entities/constants'; import { EntityDetailsHeader } from 'components/Entities/EntityDetailsHeader'; import { EntityVersions } from 'components/Entities/EntityVersions'; import { RouteComponentProps } from 'react-router-dom'; +import { LoadingSpinner } from 'components/common'; const useStyles = makeStyles((_theme: Theme) => ({ verionDetailsContatiner: { @@ -63,13 +63,17 @@ const WorkflowVersionDetailsContainer: React.FC< const id = workflowId as ResourceIdentifier; const sections = entitySections[ResourceType.WORKFLOW]; - const project = useProject(workflowId.project); + const [project] = useProject(workflowId.project); const styles = useStyles(); + if (!project?.id) { + return ; + } + return ( - + <>
- + ); }; diff --git a/packages/console/src/components/common/ContentContainer.tsx b/packages/console/src/components/common/ContentContainer.tsx index 50649d32b..d01365230 100644 --- a/packages/console/src/components/common/ContentContainer.tsx +++ b/packages/console/src/components/common/ContentContainer.tsx @@ -5,7 +5,6 @@ import { contentContainerId } from 'common/constants'; import { contentMarginGridUnits, maxContainerGridWidth, - navbarGridHeight, sideNavGridWidth, } from 'common/layout'; import { ErrorBoundary } from './ErrorBoundary'; @@ -19,16 +18,15 @@ enum ContainerClasses { const useStyles = makeStyles((theme: Theme) => { const contentMargin = `${theme.spacing(contentMarginGridUnits)}px`; - const spacerHeight = `${theme.spacing(navbarGridHeight)}px`; return { root: { display: 'flex', flexDirection: 'column', - minHeight: '100vh', - padding: `${spacerHeight} ${contentMargin} 0 ${contentMargin}`, + minHeight: `100dvh`, + padding: `0 ${contentMargin} 0 ${contentMargin}`, [`&.${ContainerClasses.NoMargin}`]: { margin: 0, - padding: `${spacerHeight} 0 0 0`, + padding: 0, }, [`&.${ContainerClasses.Centered}`]: { margin: '0 auto', @@ -66,7 +64,7 @@ export const ContentContainer: React.FC = props => { const styles = useStyles(); const { center = false, - noMargin = false, + noMargin = true, className: additionalClassName, children, sideNav = false, diff --git a/packages/console/src/components/common/DetailsPanel.tsx b/packages/console/src/components/common/DetailsPanel.tsx index 398c9284b..7c1cd0283 100644 --- a/packages/console/src/components/common/DetailsPanel.tsx +++ b/packages/console/src/components/common/DetailsPanel.tsx @@ -9,6 +9,7 @@ import { detailsPanelWidth } from './constants'; const useStyles = makeStyles((theme: Theme) => ({ modal: { pointerEvents: 'none', + padding: '100px', }, paper: { display: 'flex', @@ -54,7 +55,6 @@ export const DetailsPanel: React.FC = ({ open={open} key="detailsPanel" > -
{children} diff --git a/packages/console/src/components/common/index.ts b/packages/console/src/components/common/index.ts index d1bc31151..b4fb31025 100644 --- a/packages/console/src/components/common/index.ts +++ b/packages/console/src/components/common/index.ts @@ -5,3 +5,4 @@ export { WaitForData } from './WaitForData'; export { WaitForQuery } from './WaitForQuery'; export { DetailsGroup } from './DetailsGroup'; export { ScrollableMonospaceText } from './ScrollableMonospaceText'; +export * from './LocalStoreDefaults'; diff --git a/packages/console/src/components/hooks/useProjects.ts b/packages/console/src/components/hooks/useProjects.ts index d1c3f1ef5..4e28a6ca2 100644 --- a/packages/console/src/components/hooks/useProjects.ts +++ b/packages/console/src/components/hooks/useProjects.ts @@ -1,58 +1,33 @@ -import { CacheContext } from 'components/Cache/CacheContext'; -import { ValueCache } from 'components/Cache/createCache'; -import { NotFoundError } from 'errors/fetchErrors'; import { listProjects } from 'models/Project/api'; import { Project } from 'models/Project/types'; -import { useContext } from 'react'; -import { FetchableData } from './types'; -import { useFetchableData } from './useFetchableData'; - -const fetchableKey = Symbol('ProjectsList'); -const makeProjectCacheKey = (id: string) => ({ id, collection: fetchableKey }); - -const doFetchProjects = async (cache: ValueCache) => { - const projects = await listProjects(); - // Individually cache the projects so that we can retrieve them by id - return projects.map(p => - cache.mergeValue(makeProjectCacheKey(p.id), p), - ) as Project[]; -}; +import { useMemo } from 'react'; +import { useQuery } from 'react-query'; +import isEmpty from 'lodash/isEmpty'; /** A hook for fetching the list of available projects */ -export function useProjects(): FetchableData { - const cache = useContext(CacheContext); +export function useProjects(): [Project[], Error | any] { + const query = useQuery({ + queryKey: ['projects'], + queryFn: () => listProjects(), + }); - return useFetchableData( - { - debugName: 'Projects', - useCache: true, - defaultValue: [], - doFetch: () => doFetchProjects(cache), - }, - fetchableKey, - ); + const queryData = useMemo(() => { + if (isEmpty(query.data) && !query.isSuccess) { + return [] as Project[]; + } + return query.data as Project[]; + }, [query.data]); + + return [queryData, query.error]; } /** A hook for fetching a single Project */ -export function useProject(id: string): FetchableData { - const cache = useContext(CacheContext); +export function useProject(id: string) { + const [projects, error] = useProjects(); - const doFetch = async () => { - await doFetchProjects(cache); - const project = cache.get(makeProjectCacheKey(id)) as Project; - if (!project) { - throw new NotFoundError(id); - } - return project; - }; + const project = useMemo(() => { + return projects.find(p => p.id === id); + }, [projects, id]); - return useFetchableData( - { - doFetch, - useCache: true, - debugName: 'Projects', - defaultValue: {} as Project, - }, - makeProjectCacheKey(id), - ); + return [project, error]; } diff --git a/packages/console/src/components/utils/GlobalStyles.tsx b/packages/console/src/components/utils/GlobalStyles.tsx new file mode 100644 index 000000000..f980a55b2 --- /dev/null +++ b/packages/console/src/components/utils/GlobalStyles.tsx @@ -0,0 +1,18 @@ +import React, { useEffect } from 'react'; +import { injectGlobal } from 'emotion'; + +const GlobalStyles = () => { + useEffect(() => { + injectGlobal(` + body { + overscroll-behavior: none; + } + .sr-only { + display: none; + } + `); + }, []); + return <>; +}; + +export default GlobalStyles; diff --git a/packages/console/src/index.ts b/packages/console/src/index.ts index 585e1a880..4f70a2912 100644 --- a/packages/console/src/index.ts +++ b/packages/console/src/index.ts @@ -1,7 +1,9 @@ import './common/setupProtobuf'; +import { LOCAL_PROJECT_DOMAIN, getLocalStore } from './components'; export * from './components'; export * from './routes'; export * from './models'; export * from './common'; export * from './basics'; +export { LOCAL_PROJECT_DOMAIN, getLocalStore }; diff --git a/packages/console/src/routes/ApplicationRouter.tsx b/packages/console/src/routes/ApplicationRouter.tsx index 641fb0a75..a73c76961 100644 --- a/packages/console/src/routes/ApplicationRouter.tsx +++ b/packages/console/src/routes/ApplicationRouter.tsx @@ -1,15 +1,7 @@ -import { - ContentContainer, - ContentContainerProps, -} from 'components/common/ContentContainer'; -import { withSideNavigation } from 'components/Navigation/withSideNavigation'; import React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; +import { history } from 'routes/history'; import { useExternalConfigurationContext } from 'basics/ExternalConfigurationProvider'; -import { Toolbar } from '@material-ui/core'; -import { styled } from '@material-ui/core/styles'; -import { subnavBarContentId } from 'common/constants'; -import { subnavBackgroundColor } from 'components/Theme/constants'; import { makeRoute } from '@flyteorg/common'; import { getLocalStore, @@ -19,106 +11,113 @@ import { import { components } from './components'; import { Routes } from './routes'; -const StyledSubNavBarContent = styled(Toolbar)(() => ({ - minHeight: 'auto', - padding: 0, - margin: 0, +/** + * Perform an animation when the route changes + * Currently only resets scroll + * @param history + * @returns + */ +const AnimateRoute = ({ history }) => { + const from = React.useRef(window.location); - '& > *': { - alignItems: 'center', - display: 'flex', - maxWidth: '100%', - padding: '24px 20px 24px 30px', - background: subnavBackgroundColor, - }, - '@media (min-width: 600px)': { - minHeight: 'auto', - }, -})); + const scrollToTop = () => { + setTimeout(() => { + window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); + }, 0); + }; -export function withContentContainer

( - WrappedComponent: React.FC

, - contentContainerProps?: ContentContainerProps, -) { - return (props: P) => ( - - + React.useEffect(() => { + const historyAction = history.listen((to, action) => { + if (action === 'PUSH') { + // link click + return scrollToTop(); + } - - - ); -} + if (action === 'POP' && from.current.pathname !== to.pathname) { + // browser back button + // only scroll to top if the path is different + // ignore query params or hash changes + return scrollToTop(); + } + + // update from location + from.current = to.pathname; + }); + + return () => { + historyAction(); + }; + }, []); + + return <>; +}; export const ApplicationRouter: React.FC = () => { const localProjectDomain = getLocalStore( LOCAL_PROJECT_DOMAIN, ) as LocalStorageProjectDomain; - const additionalRoutes = - useExternalConfigurationContext()?.registry?.additionalRoutes || null; + useExternalConfigurationContext()?.registry?.additionalRoutes || []; return ( - - {additionalRoutes} - - - - - - - - { - /** - * If LocalStoreDefaults exist, we direct them to the project detail view - * for those values. - */ - if (localProjectDomain) { - return ( - - ); - } else { + <> + + {additionalRoutes?.length && additionalRoutes.map(route => route)} + + + + + + + + { + /** + * If LocalStoreDefaults exist, we direct them to the project detail view + * for those values. + */ + if (localProjectDomain) { + return ( + + ); + } + return ; - } - }} - /> - - + }} + /> + + + + ); }; diff --git a/packages/console/src/routes/routes.ts b/packages/console/src/routes/routes.ts index 70b4ed8c3..bc20b97ea 100644 --- a/packages/console/src/routes/routes.ts +++ b/packages/console/src/routes/routes.ts @@ -4,10 +4,11 @@ import { makeRoute } from '@flyteorg/common'; import { projectBasePath, projectDomainBasePath } from './constants'; /** Creates a path relative to a particular project */ -export const makeProjectBoundPath = (projectId: string, path = '') => - makeRoute( +export const makeProjectBoundPath = (projectId: string, path = '') => { + return makeRoute( `/projects/${projectId}${path.length ? ensureSlashPrefixed(path) : path}`, ); +}; /** Creates a path relative to a particular project and domain. Paths should begin with a slash (/) */ export const makeProjectDomainBoundPath = ( diff --git a/script/test/jest.base.js b/script/test/jest.base.js index f476bca5f..23c160e7f 100644 --- a/script/test/jest.base.js +++ b/script/test/jest.base.js @@ -7,7 +7,7 @@ module.exports = { testPathIgnorePatterns: [ '__stories__', '.storybook', - 'node_modules', + '/node_modules/', 'dist', 'lib', 'build', diff --git a/website/env.js b/website/env.js index 8a8bbe8b5..13648d38c 100644 --- a/website/env.js +++ b/website/env.js @@ -41,6 +41,8 @@ const GA_TRACKING_ID = process.env.GA_TRACKING_ID || ''; const FLYTE_NAVIGATION = process.env.FLYTE_NAVIGATION || ''; +const HORIZONTAL_LAYOUT = process.env.HORIZONTAL_LAYOUT || 'false'; + module.exports = { ADMIN_API_URL, ADMIN_API_USE_SSL, @@ -53,6 +55,7 @@ module.exports = { ASSETS_PATH, CERTIFICATE_PATH, LOCAL_DEV_HOST, + HORIZONTAL_LAYOUT, processEnv: { ADMIN_API_URL, BASE_URL, @@ -61,5 +64,6 @@ module.exports = { NODE_ENV, STATUS_URL, FLYTE_NAVIGATION, + HORIZONTAL_LAYOUT, }, };