From aaded00accd742ef6cdfb0398e985a0fd8d114f4 Mon Sep 17 00:00:00 2001 From: olga-union <101579322+olga-union@users.noreply.github.com> Date: Tue, 5 Apr 2022 17:53:28 -0500 Subject: [PATCH] feat: project domain default page (#352) * feat: project domain default page * feat: add tests, address feedback * feat: always render numbers block Signed-off-by: Olga Nad --- package.json | 2 +- .../ExecutionDetailsAppBarContent.tsx | 4 +- .../Navigation/ProjectNavigation.tsx | 26 ++-- ...ectExecutions.tsx => ProjectDashboard.tsx} | 132 ++++++++++++------ src/components/Project/ProjectDetails.tsx | 12 +- src/components/Project/strings.ts | 11 ++ ...ons.test.tsx => ProjectDashboard.test.tsx} | 35 ++++- src/components/common/DataTable.tsx | 56 ++++++++ .../common/DomainSettingsSection.tsx | 104 ++++++++++++++ src/components/common/strings.ts | 16 +++ src/components/common/test/DataTable.test.tsx | 22 +++ .../test/DomainSettingsSection.test.tsx | 93 ++++++++++++ src/models/Common/constants.ts | 1 + src/models/Project/api.ts | 18 +++ src/models/Project/utils.ts | 8 ++ src/routes/routes.ts | 4 +- yarn.lock | 8 +- 17 files changed, 480 insertions(+), 72 deletions(-) rename src/components/Project/{ProjectExecutions.tsx => ProjectDashboard.tsx} (50%) create mode 100644 src/components/Project/strings.ts rename src/components/Project/test/{ProjectExecutions.test.tsx => ProjectDashboard.test.tsx} (83%) create mode 100644 src/components/common/DataTable.tsx create mode 100644 src/components/common/DomainSettingsSection.tsx create mode 100644 src/components/common/strings.ts create mode 100644 src/components/common/test/DataTable.test.tsx create mode 100644 src/components/common/test/DomainSettingsSection.test.tsx diff --git a/package.json b/package.json index c0133a7f2..4b54ebb1c 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "@commitlint/cli": "^8.3.5", "@commitlint/config-conventional": "^8.3.4", "@date-io/moment": "1.3.9", - "@flyteorg/flyteidl": "0.23.1", + "@flyteorg/flyteidl": "0.24.11", "@material-ui/core": "^4.0.0", "@material-ui/icons": "^4.0.0", "@material-ui/pickers": "^3.2.2", diff --git a/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx b/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx index 2fc11f5b3..8c07f90d2 100644 --- a/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx +++ b/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx @@ -92,11 +92,11 @@ export const ExecutionDetailsAppBarContent: React.FC<{ const onCloseRelaunch = () => setShowRelaunchForm(false); const fromExecutionNav = new URLSearchParams(history.location.search).get('fromExecutionNav'); const backLink = fromExecutionNav - ? Routes.ProjectDetails.sections.executions.makeUrl(project, domain) + ? Routes.ProjectDetails.sections.dashboard.makeUrl(project, domain) : originalBackLink; const { recoverExecution, - recoverState: { isLoading: recovering, error, data: recoveredId }, + recoverState: { isLoading: recovering, data: recoveredId }, } = useRecoverExecutionState(); React.useEffect(() => { diff --git a/src/components/Navigation/ProjectNavigation.tsx b/src/components/Navigation/ProjectNavigation.tsx index 357781931..b4b75f645 100644 --- a/src/components/Navigation/ProjectNavigation.tsx +++ b/src/components/Navigation/ProjectNavigation.tsx @@ -3,7 +3,7 @@ import { SvgIconProps } from '@material-ui/core/SvgIcon'; import ChevronRight from '@material-ui/icons/ChevronRight'; import DeviceHub from '@material-ui/icons/DeviceHub'; import LinearScale from '@material-ui/icons/LinearScale'; -import TrendingFlat from '@material-ui/icons/TrendingFlat'; +import Dashboard from '@material-ui/icons/Dashboard'; import classnames from 'classnames'; import { useCommonStyles } from 'components/common/styles'; import { withRouteParams } from 'components/common/withRouteParams'; @@ -72,46 +72,46 @@ const ProjectNavigationImpl: React.FC = ({ const routes: ProjectRoute[] = [ { - icon: DeviceHub, + icon: Dashboard, isActive: (match, location) => { const finalMatch = match ? match : matchPath(location.pathname, { - path: Routes.WorkflowDetails.path, + path: Routes.ProjectDashboard.path, exact: false, }); return !!finalMatch; }, - path: Routes.ProjectDetails.sections.workflows.makeUrl(project.value.id, domainId), - text: 'Workflows', + path: Routes.ProjectDetails.sections.dashboard.makeUrl(project.value.id, domainId), + text: 'Project Dashboard', }, { - icon: LinearScale, + icon: DeviceHub, isActive: (match, location) => { const finalMatch = match ? match : matchPath(location.pathname, { - path: Routes.TaskDetails.path, + path: Routes.WorkflowDetails.path, exact: false, }); return !!finalMatch; }, - path: Routes.ProjectDetails.sections.tasks.makeUrl(project.value.id, domainId), - text: 'Tasks', + path: Routes.ProjectDetails.sections.workflows.makeUrl(project.value.id, domainId), + text: 'Workflows', }, { - icon: TrendingFlat, + icon: LinearScale, isActive: (match, location) => { const finalMatch = match ? match : matchPath(location.pathname, { - path: Routes.ProjectExecutions.path, + path: Routes.TaskDetails.path, exact: false, }); return !!finalMatch; }, - path: Routes.ProjectDetails.sections.executions.makeUrl(project.value.id, domainId), - text: 'Executions', + path: Routes.ProjectDetails.sections.tasks.makeUrl(project.value.id, domainId), + text: 'Tasks', }, ]; diff --git a/src/components/Project/ProjectExecutions.tsx b/src/components/Project/ProjectDashboard.tsx similarity index 50% rename from src/components/Project/ProjectExecutions.tsx rename to src/components/Project/ProjectDashboard.tsx index 08da705f8..3f42c51f5 100644 --- a/src/components/Project/ProjectExecutions.tsx +++ b/src/components/Project/ProjectDashboard.tsx @@ -1,5 +1,12 @@ -import Typography from '@material-ui/core/Typography'; import { makeStyles, Theme } from '@material-ui/core/styles'; +import * as React from 'react'; +import { Typography } from '@material-ui/core'; +import { useTaskNameList, useWorkflowNameList } from 'components/hooks/useNamedEntity'; +import { useWorkflowExecutions } from 'components/hooks/useWorkflowExecutions'; +import { WaitForQuery } from 'components/common/WaitForQuery'; +import { useInfiniteQuery, useQuery, useQueryClient } from 'react-query'; +import { Admin } from 'flyteidl'; +import { DomainSettingsSection } from 'components/common/DomainSettingsSection'; import { getCacheKey } from 'components/Cache/utils'; import { ErrorBoundary } from 'components/common/ErrorBoundary'; import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; @@ -11,24 +18,30 @@ import { makeWorkflowExecutionListQuery } from 'components/Executions/workflowEx import { SortDirection } from 'models/AdminEntity/types'; import { executionSortFields } from 'models/Execution/constants'; import { Execution } from 'models/Execution/types'; -import * as React from 'react'; -import { useInfiniteQuery } from 'react-query'; import { BarChart } from 'components/common/BarChart'; import { getExecutionTimeData, getStartExecutionTime, } from 'components/Entities/EntityExecutionsBarChart'; import classNames from 'classnames'; -import { useWorkflowExecutions } from 'components/hooks/useWorkflowExecutions'; import { useExecutionShowArchivedState } from 'components/Executions/filters/useExecutionArchiveState'; import { useOnlyMyExecutionsFilterState } from 'components/Executions/filters/useOnlyMyExecutionsFilterState'; import { WaitForData } from 'components/common/WaitForData'; import { history } from 'routes/history'; import { Routes } from 'routes/routes'; import { compact } from 'lodash'; +import { getProjectDomainAttributes } from 'models/Project/api'; +import t from './strings'; import { failedToLoadExecutionsString } from './constants'; const useStyles = makeStyles((theme: Theme) => ({ + projectStats: { + paddingTop: theme.spacing(7), + paddingBottom: theme.spacing(7), + display: 'flex', + justifyContent: 'space-evenly', + alignItems: 'center', + }, container: { display: 'flex', flex: '1 1 auto', @@ -48,7 +61,8 @@ const useStyles = makeStyles((theme: Theme) => ({ paddingTop: theme.spacing(1), }, })); -export interface ProjectExecutionsProps { + +export interface ProjectDashboardProps { projectId: string; domainId: string; } @@ -58,8 +72,7 @@ const defaultSort = { direction: SortDirection.DESCENDING, }; -/** A listing of all executions across a project/domain combination. */ -export const ProjectExecutions: React.FC = ({ +export const ProjectDashboard: React.FC = ({ domainId: domain, projectId: project, }) => { @@ -90,7 +103,7 @@ export const ProjectExecutions: React.FC = ({ [domain, project, allFilters], ); - const query = useInfiniteQuery({ + const executionsQuery = useInfiniteQuery({ ...makeWorkflowExecutionListQuery({ domain, project }, config), }); @@ -98,10 +111,10 @@ export const ProjectExecutions: React.FC = ({ // flat list. const executions = React.useMemo( () => - query.data?.pages - ? query.data.pages.reduce((acc, { data }) => acc.concat(data), []) + executionsQuery.data?.pages + ? executionsQuery.data.pages.reduce((acc, { data }) => acc.concat(data), []) : [], - [query.data?.pages], + [executionsQuery.data?.pages], ); const handleBarChartItemClick = React.useCallback((item) => { @@ -118,50 +131,89 @@ export const ProjectExecutions: React.FC = ({ }, ); - const fetch = React.useCallback(() => query.fetchNextPage(), [query]); + const fetch = React.useCallback(() => executionsQuery.fetchNextPage(), [executionsQuery]); + + const { value: workflows } = useWorkflowNameList({ domain, project }, {}); + const numberOfWorkflows = workflows.length; + const { value: tasks } = useTaskNameList({ domain, project }, {}); + const numberOfTasks = tasks.length; - const content = query.isLoadingError ? ( - - ) : query.isLoading ? ( + const queryClient = useQueryClient(); + + const projectDomainAttributesQuery = useQuery({ + queryKey: ['projectDomainAttributes', project, domain], + queryFn: async () => { + const projectDomainAtributes = await getProjectDomainAttributes({ domain, project }); + queryClient.setQueryData( + ['projectDomainAttributes', project, domain], + projectDomainAtributes, + ); + return projectDomainAtributes; + }, + staleTime: Infinity, + }); + + const content = executionsQuery.isLoadingError ? ( + + ) : executionsQuery.isLoading ? ( ) : ( ); + const configData = + projectDomainAttributesQuery.data?.attributes?.matchingAttributes?.workflowExecutionConfig ?? + undefined; + + const renderDomainSettingsSection = () => ; + return (
- - Last 100 Executions in the Project - -
- - - +
+ {t('workflowsTotal', numberOfWorkflows)} + {t('tasksTotal', numberOfTasks)} +
+ + {renderDomainSettingsSection} + +
+ + {t('last100ExecutionsTitle')} + +
+ + + +
+ + {t('allExecutionsTitle')} + + + {content}
- - All Executions in the Project - - - {content}
); }; diff --git a/src/components/Project/ProjectDetails.tsx b/src/components/Project/ProjectDetails.tsx index 2751fd4ab..96f346856 100644 --- a/src/components/Project/ProjectDetails.tsx +++ b/src/components/Project/ProjectDetails.tsx @@ -8,7 +8,7 @@ import { Project } from 'models/Project/types'; import * as React from 'react'; import { Redirect, Route, Switch } from 'react-router'; import { Routes } from 'routes/routes'; -import { ProjectExecutions } from './ProjectExecutions'; +import { ProjectDashboard } from './ProjectDashboard'; import { ProjectTasks } from './ProjectTasks'; import { ProjectWorkflows } from './ProjectWorkflows'; @@ -27,7 +27,7 @@ export interface ProjectDetailsRouteParams { export type ProjectDetailsProps = ProjectDetailsRouteParams; const entityTypeToComponent = { - executions: ProjectExecutions, + executions: ProjectDashboard, tasks: ProjectTasks, workflows: ProjectWorkflows, }; @@ -59,7 +59,7 @@ const ProjectEntitiesByDomain: React.FC<{ ); }; -const ProjectExecutionsByDomain: React.FC<{ project: Project }> = ({ project }) => ( +const ProjectDashboardByDomain: React.FC<{ project: Project }> = ({ project }) => ( ); @@ -79,15 +79,15 @@ export const ProjectDetailsContainer: React.FC = ({ p {() => { return ( + + + - - - ); diff --git a/src/components/Project/strings.ts b/src/components/Project/strings.ts new file mode 100644 index 000000000..6fd42c3ed --- /dev/null +++ b/src/components/Project/strings.ts @@ -0,0 +1,11 @@ +import { createLocalizedString } from 'basics/Locale'; + +const str = { + allExecutionsTitle: 'All Executions in the Project', + last100ExecutionsTitle: 'Last 100 Executions in the Project', + tasksTotal: (n: number) => `${n} Tasks`, + workflowsTotal: (n: number) => `${n} Workflows`, +}; + +export { patternKey } from 'basics/Locale'; +export default createLocalizedString(str); diff --git a/src/components/Project/test/ProjectExecutions.test.tsx b/src/components/Project/test/ProjectDashboard.test.tsx similarity index 83% rename from src/components/Project/test/ProjectExecutions.test.tsx rename to src/components/Project/test/ProjectDashboard.test.tsx index 4772c5119..6c76b68f8 100644 --- a/src/components/Project/test/ProjectExecutions.test.tsx +++ b/src/components/Project/test/ProjectDashboard.test.tsx @@ -17,16 +17,35 @@ import { createTestQueryClient, disableQueryLogger, enableQueryLogger } from 'te import { APIContext } from 'components/data/apiContext'; import { mockAPIContextValue } from 'components/data/__mocks__/apiContext'; import { getUserProfile } from 'models/Common/api'; -import { ProjectExecutions } from '../ProjectExecutions'; +import { getProjectDomainAttributes } from 'models/Project/api'; +import { Admin } from 'flyteidl'; +import { ProjectDashboard } from '../ProjectDashboard'; import { failedToLoadExecutionsString } from '../constants'; jest.mock('components/Executions/Tables/WorkflowExecutionsTable'); -// jest.mock('components/common/LoadingSpinner'); jest.mock('notistack', () => ({ useSnackbar: () => ({ enqueueSnackbar: jest.fn() }), })); -describe('ProjectExecutions', () => { +const projectDomainAttributesMock: Admin.ProjectDomainAttributesDeleteResponse = { + attributes: { + matchingAttributes: { + workflowExecutionConfig: { + maxParallelism: 5, + securityContext: { runAs: { k8sServiceAccount: 'default' } }, + rawOutputDataConfig: { outputLocationPrefix: 'cliOutputLocationPrefix' }, + annotations: { values: { cliAnnotationKey: 'cliAnnotationValue' } }, + labels: { values: { cliLabelKey: 'cliLabelValue' } }, + }, + }, + }, +}; + +jest.mock('models/Project/api', () => ({ + getProjectDomainAttributes: jest.fn().mockResolvedValue(projectDomainAttributesMock), +})); + +describe('ProjectDashboard', () => { let basicPythonFixture: ReturnType; let failedTaskFixture: ReturnType; let executions1: Execution[]; @@ -76,12 +95,20 @@ describe('ProjectExecutions', () => { getUserProfile: mockGetUserProfile, })} > - + , { wrapper: MemoryRouter }, ); + it('should display domain attributes section when config was provided', async () => { + const { getByText } = renderView(); + expect(getProjectDomainAttributes).toHaveBeenCalled(); + await waitFor(() => { + expect(getByText('Domain Settings')).toBeInTheDocument(); + }); + }); + it('should show loading spinner', async () => { mockGetUserProfile.mockResolvedValue(sampleUserProfile); const { queryByTestId } = renderView(); diff --git a/src/components/common/DataTable.tsx b/src/components/common/DataTable.tsx new file mode 100644 index 000000000..6f12a421e --- /dev/null +++ b/src/components/common/DataTable.tsx @@ -0,0 +1,56 @@ +import { makeStyles, Theme } from '@material-ui/core/styles'; +import * as React from 'react'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from '@material-ui/core'; +import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum'; + +const useStyles = makeStyles((theme: Theme) => ({ + headerCell: { + padding: theme.spacing(1, 0, 1, 0), + color: COLOR_SPECTRUM.gray40.color, + }, + cell: { + padding: theme.spacing(1, 0, 1, 0), + minWidth: '100px', + }, + cellLeft: { + padding: theme.spacing(1, 1, 1, 0), + minWidth: '100px', + }, +})); + +export interface DataTableProps { + data: { [k: string]: string }; +} + +export const DataTable: React.FC = ({ data }) => { + const styles = useStyles(); + + return ( + + + + + Key + Value + + + + {Object.keys(data).map((key) => ( + + {key} + {data[key]} + + ))} + +
+
+ ); +}; diff --git a/src/components/common/DomainSettingsSection.tsx b/src/components/common/DomainSettingsSection.tsx new file mode 100644 index 000000000..54d1901c8 --- /dev/null +++ b/src/components/common/DomainSettingsSection.tsx @@ -0,0 +1,104 @@ +import { makeStyles, Theme } from '@material-ui/core/styles'; +import * as React from 'react'; +import { Typography } from '@material-ui/core'; +import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum'; +import { DataTable } from 'components/common/DataTable'; +import { Admin } from 'flyteidl'; +import { isEmpty } from 'lodash'; +import t from './strings'; + +const useStyles = makeStyles((theme: Theme) => ({ + domainSettingsWrapper: { + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + }, + domainSettings: { + marginTop: theme.spacing(1), + borderTop: `1px solid ${COLOR_SPECTRUM.gray15.color}`, + padding: theme.spacing(2, 4, 0, 4), + display: 'flex', + justifyContent: 'space-between', + }, + sectionHeader: { + margin: 0, + fontWeight: 700, + fontSize: '16px', + }, + subHeader: { + margin: 0, + paddingBottom: theme.spacing(2), + fontSize: '16px', + fontWeight: 600, + }, + grayText: { + padding: theme.spacing(1, 0, 1, 0), + color: COLOR_SPECTRUM.gray40.color, + }, +})); + +interface DomainSettingsSectionProps { + configData?: Admin.IWorkflowExecutionConfig; +} + +export const DomainSettingsSection = ({ configData }: DomainSettingsSectionProps) => { + const styles = useStyles(); + if (!configData || isEmpty(configData)) { + return null; + } + + const role = configData.securityContext?.runAs?.iamRole || t('inherited'); + const serviceAccount = configData.securityContext?.runAs?.k8sServiceAccount || t('inherited'); + const rawData = configData.rawOutputDataConfig?.outputLocationPrefix || t('inherited'); + const maxParallelism = configData.maxParallelism || undefined; + + return ( +
+

{t('domainSettingsTitle')}

+
+
+

{t('securityContextHeader')}

+
+ + {t('iamRoleHeader')} + + {role} +
+
+ + {t('serviceAccountHeader')} + + {serviceAccount} +
+
+
+

{t('labelsHeader')}

+ {configData.labels?.values ? ( + + ) : ( + t('inherited') + )} +
+
+

{t('annotationsHeader')}

+ {configData.annotations?.values ? ( + + ) : ( + t('inherited') + )} +
+
+
+

{t('rawDataHeader')}

+ {rawData} +
+
+

+ {t('maxParallelismHeader')} +

+ {maxParallelism ?? t('inherited')} +
+
+
+
+ ); +}; diff --git a/src/components/common/strings.ts b/src/components/common/strings.ts new file mode 100644 index 000000000..33a3e0f0b --- /dev/null +++ b/src/components/common/strings.ts @@ -0,0 +1,16 @@ +import { createLocalizedString } from 'basics/Locale'; + +const str = { + annotationsHeader: 'Annotations', + domainSettingsTitle: 'Domain Settings', + iamRoleHeader: 'IAM Role', + inherited: 'Inherits from project level values', + labelsHeader: 'Labels', + maxParallelismHeader: 'Max parallelism', + rawDataHeader: 'Raw output data config', + securityContextHeader: 'Security Context', + serviceAccountHeader: 'Service Account', +}; + +export { patternKey } from 'basics/Locale'; +export default createLocalizedString(str); diff --git a/src/components/common/test/DataTable.test.tsx b/src/components/common/test/DataTable.test.tsx new file mode 100644 index 000000000..4ed7fe902 --- /dev/null +++ b/src/components/common/test/DataTable.test.tsx @@ -0,0 +1,22 @@ +import { render } from '@testing-library/react'; +import * as React from 'react'; + +import { DataTable } from '../DataTable'; + +const mockData = { key1: 'value1', key2: 'value2' }; + +describe('DataTable', () => { + it('should render a table with mocked data', () => { + const { getAllByRole } = render(); + const headers = getAllByRole('columnheader'); + const cells = getAllByRole('cell'); + expect(headers).toHaveLength(2); + expect(headers[0]).toHaveTextContent('Key'); + expect(headers[1]).toHaveTextContent('Value'); + expect(cells).toHaveLength(4); + expect(cells[0]).toHaveTextContent('key1'); + expect(cells[1]).toHaveTextContent('value1'); + expect(cells[2]).toHaveTextContent('key2'); + expect(cells[3]).toHaveTextContent('value2'); + }); +}); diff --git a/src/components/common/test/DomainSettingsSection.test.tsx b/src/components/common/test/DomainSettingsSection.test.tsx new file mode 100644 index 000000000..74433e715 --- /dev/null +++ b/src/components/common/test/DomainSettingsSection.test.tsx @@ -0,0 +1,93 @@ +import { render } from '@testing-library/react'; +import * as React from 'react'; + +import { DomainSettingsSection } from '../DomainSettingsSection'; + +const serviceAccount = 'default'; +const rawData = 'cliOutputLocationPrefix'; +const maxParallelism = 10; + +const mockConfigData = { + maxParallelism: maxParallelism, + securityContext: { runAs: { k8sServiceAccount: serviceAccount } }, + rawOutputDataConfig: { outputLocationPrefix: rawData }, + annotations: { values: { cliAnnotationKey: 'cliAnnotationValue' } }, + labels: { values: { cliLabelKey: 'cliLabelValue' } }, +}; + +const mockConfigDataWithoutLabels = { + maxParallelism: maxParallelism, + securityContext: { runAs: { k8sServiceAccount: serviceAccount } }, + rawOutputDataConfig: { outputLocationPrefix: rawData }, + annotations: { values: { cliAnnotationKey: 'cliAnnotationValue' } }, +}; + +const mockConfigDataWithoutLabelsAndAnnotations = { + maxParallelism: maxParallelism, + securityContext: { runAs: { k8sServiceAccount: serviceAccount } }, + rawOutputDataConfig: { outputLocationPrefix: rawData }, +}; + +describe('DomainSettingsSection', () => { + it('should not render a block if config data passed is empty', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it('should render a section without IAMRole data', () => { + const { queryByText, queryAllByRole } = render( + , + ); + expect(queryByText('Domain Settings')).toBeInTheDocument(); + // should display serviceAccount value + expect(queryByText(serviceAccount)).toBeInTheDocument(); + // should display rawData value + expect(queryByText(rawData)).toBeInTheDocument(); + // should display maxParallelism value + expect(queryByText(maxParallelism)).toBeInTheDocument(); + // should display 2 data tables + const tables = queryAllByRole('table'); + expect(tables).toHaveLength(2); + // should display a placeholder text, as role was not passed + const emptyRole = queryByText('Inherits from project level values'); + expect(emptyRole).toBeInTheDocument(); + }); + + it('should render a section without IAMRole and Labels data', () => { + const { queryByText, queryAllByText, queryAllByRole } = render( + , + ); + expect(queryByText('Domain Settings')).toBeInTheDocument(); + // should display serviceAccount value + expect(queryByText(serviceAccount)).toBeInTheDocument(); + // should display rawData value + expect(queryByText(rawData)).toBeInTheDocument(); + // should display maxParallelism value + expect(queryByText(maxParallelism)).toBeInTheDocument(); + // should display 1 data table + const tables = queryAllByRole('table'); + expect(tables).toHaveLength(1); + // should display two placeholder text, as role and labels were not passed + const inheritedPlaceholders = queryAllByText('Inherits from project level values'); + expect(inheritedPlaceholders).toHaveLength(2); + }); + + it('should render a section without IAMRole, Labels, Annotations data', () => { + const { queryByText, queryAllByText, queryByRole } = render( + , + ); + expect(queryByText('Domain Settings')).toBeInTheDocument(); + // should display serviceAccount value + expect(queryByText(serviceAccount)).toBeInTheDocument(); + // should display rawData value + expect(queryByText(rawData)).toBeInTheDocument(); + // should display maxParallelism value + expect(queryByText(maxParallelism)).toBeInTheDocument(); + // should not display any data tables + const tables = queryByRole('table'); + expect(tables).not.toBeInTheDocument(); + // should display three placeholder text, as role, labels, annotations were not passed + const inheritedPlaceholders = queryAllByText('Inherits from project level values'); + expect(inheritedPlaceholders).toHaveLength(3); + }); +}); diff --git a/src/models/Common/constants.ts b/src/models/Common/constants.ts index b40484545..df752491c 100644 --- a/src/models/Common/constants.ts +++ b/src/models/Common/constants.ts @@ -11,6 +11,7 @@ export const endpointPrefixes = { nodeExecution: '/node_executions', dynamicWorkflowExecution: '/data/node_executions', project: '/projects', + projectDomainAtributes: '/project_domain_attributes', relaunchExecution: '/executions/relaunch', recoverExecution: '/executions/recover', task: '/tasks', diff --git a/src/models/Project/api.ts b/src/models/Project/api.ts index b86cf6c55..278f40144 100644 --- a/src/models/Project/api.ts +++ b/src/models/Project/api.ts @@ -3,7 +3,9 @@ import { sortBy } from 'lodash'; import { endpointPrefixes } from 'models/Common/constants'; import { getAdminEntity } from 'models/AdminEntity/AdminEntity'; +import { IdentifierScope } from 'models/Common/types'; import { Project } from './types'; +import { makeProjectDomainAttributesPath } from './utils'; /** Fetches the list of available `Project`s */ export const listProjects = () => @@ -15,3 +17,19 @@ export const listProjects = () => transform: ({ projects }: Admin.Projects) => sortBy(projects, (project) => `${project.name}`.toLowerCase()) as Project[], }); + +export const getProjectDomainAttributes = (scope: IdentifierScope) => + getAdminEntity< + Admin.ProjectDomainAttributesGetResponse, + Admin.ProjectDomainAttributesGetResponse + >( + { + path: makeProjectDomainAttributesPath(endpointPrefixes.projectDomainAtributes, scope), + messageType: Admin.ProjectDomainAttributesGetResponse, + }, + { + params: { + resource_type: 'WORKFLOW_EXECUTION_CONFIG', + }, + }, + ); diff --git a/src/models/Project/utils.ts b/src/models/Project/utils.ts index 59b5c7de6..6a17f7de6 100644 --- a/src/models/Project/utils.ts +++ b/src/models/Project/utils.ts @@ -1,3 +1,4 @@ +import { Identifier } from 'models/Common/types'; import { Project } from './types'; export function getProjectDomain(project: Project, domainId: string) { @@ -7,3 +8,10 @@ export function getProjectDomain(project: Project, domainId: string) { } return domain; } + +export function makeProjectDomainAttributesPath( + prefix: string, + { project, domain }: Partial, +) { + return [prefix, project, domain].join('/'); +} diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 65cbcc8ff..8588fb4cd 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -19,7 +19,7 @@ export class Routes { makeProjectBoundPath(project, section ? `/${section}` : ''), path: projectBasePath, sections: { - executions: { + dashboard: { makeUrl: (project: string, domain?: string) => makeProjectBoundPath(project, `/executions${domain ? `?domain=${domain}` : ''}`), path: `${projectBasePath}/executions`, @@ -37,7 +37,7 @@ export class Routes { }, }; - static ProjectExecutions = { + static ProjectDashboard = { makeUrl: (project: string, domain: string) => makeProjectDomainBoundPath(project, domain, '/executions'), path: `${projectDomainBasePath}/executions`, diff --git a/yarn.lock b/yarn.lock index 4d7664047..a0ad6f557 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1731,10 +1731,10 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@flyteorg/flyteidl@0.23.1": - version "0.23.1" - resolved "https://registry.yarnpkg.com/@flyteorg/flyteidl/-/flyteidl-0.23.1.tgz#da88166e1c0bd404a4f00db425fe07afaaa76a8e" - integrity sha512-3M6o3EObbE35cFoY88/DNPcKEp+g2mOuFc2DNlCFntKH1lsB+EEl9k+chuWy8F8iWe7DHkCKJvM5AeEjSseahg== +"@flyteorg/flyteidl@0.24.11": + version "0.24.11" + resolved "https://registry.yarnpkg.com/@flyteorg/flyteidl/-/flyteidl-0.24.11.tgz#a1a6e17a0cd9303cf335fae50162d66c6a0ed980" + integrity sha512-PBRei4Yu5wHp8Fa683EW2xYllyApNcZOZwEh7f1tf68tnhrrvV455HBKez2U1qcqFg179qTy7VNX1zvjGCMmuA== "@gar/promisify@^1.0.1": version "1.1.3"