diff --git a/src/components/ListTable.tsx b/src/components/ListTable.tsx index d22684a0..b73bfd7b 100644 --- a/src/components/ListTable.tsx +++ b/src/components/ListTable.tsx @@ -19,6 +19,7 @@ interface ListTableProps extends EnhancedTableProps { to?: string customButton?: React.ReactElement customButtonText?: React.ReactElement + createButtonDisabled?: boolean hasDropdownFilter?: boolean dropdownFilterLabel?: string @@ -39,6 +40,7 @@ export default function ({ to, customButton = null, customButtonText = null, + createButtonDisabled = false, hasDropdownFilter = false, dropdownFilterLabel = 'Filter', @@ -66,7 +68,6 @@ export default function ({ const [internalSelected, setInternalSelected] = useState('') const selectedFilter = dropdownFilterValue ?? internalSelected - // When items load/change, select first item if nothing selected useEffect(() => { if (!hasDropdownFilter) return if (selectedFilter) return @@ -100,7 +101,13 @@ export default function ({ {(isPlatformAdmin || oboTeamId) && !noCrud && ( - diff --git a/src/pages/builds/create-edit/BuildsCreateEditPage.tsx b/src/pages/builds/create-edit/BuildsCreateEditPage.tsx index 2d13160a..c7320ab3 100644 --- a/src/pages/builds/create-edit/BuildsCreateEditPage.tsx +++ b/src/pages/builds/create-edit/BuildsCreateEditPage.tsx @@ -2,7 +2,7 @@ import { Box, Grid, Typography, useTheme } from '@mui/material' import { TextField } from 'components/forms/TextField' import PaperLayout from 'layouts/Paper' import React, { useEffect, useMemo, useState } from 'react' -import { Redirect, RouteComponentProps } from 'react-router-dom' +import { Redirect, RouteComponentProps, useHistory } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useAppSelector } from 'redux/hooks' import { @@ -30,6 +30,9 @@ import ControlledCheckbox from 'components/forms/ControlledCheckbox' import { Autocomplete } from 'components/forms/Autocomplete' import { useSession } from 'providers/Session' import { LoadingButton } from '@mui/lab' +import InformationBanner from 'components/InformationBanner' +import MuiLink from 'components/MuiLink' +import useSettings from 'hooks/useSettings' import { aplBuildApiSchema } from './create-edit-builds.validator' const getBuildName = (name: string, tag: string): string => { @@ -61,8 +64,34 @@ export default function BuildsCreateEditPage({ cluster: { domainSuffix }, }, appsEnabled, + user: { isPlatformAdmin }, } = useSession() + const { onToggleView } = useSettings() + const history = useHistory() + + const appsMissing = !appsEnabled.tekton || !appsEnabled.harbor + + const handleAppsClick = (e: React.MouseEvent) => { + e.preventDefault() + + onToggleView() + + history.push('/apps/admin') + } + + const bannerMessage = isPlatformAdmin ? ( + <> + Container Images requires Tekton and Harbor to be enabled. Click{' '} + + here + {' '} + to enable them. + + ) : ( + 'Admin needs to enable the Tekton and Harbor app to activate this feature.' + ) + const options = [ { value: 'docker', label: 'Docker', imgSrc: '/logos/docker_logo.svg' }, { value: 'buildpacks', label: 'BuildPacks', imgSrc: '/logos/buildpacks_logo.svg' }, @@ -92,7 +121,7 @@ export default function BuildsCreateEditPage({ [codeRepos, appsEnabled?.gitea], ) - const isDirty = useAppSelector(({ global: { isDirty } }) => isDirty) + const isDirty = useAppSelector((state) => state.global?.isDirty) useEffect(() => { if (isDirty !== false) return if (!isFetching) refetch() @@ -163,6 +192,8 @@ export default function BuildsCreateEditPage({ if (!mutating && isSuccessCreate) return const onSubmit = (formData: CreateAplBuildApiResponse) => { + if (appsMissing) return + const imageName = formData.spec?.imageName ?? '' const tag = formData.spec?.tag ?? '' @@ -206,200 +237,212 @@ export default function BuildsCreateEditPage({ return ( - - - -
-
- Select build task - - - { - const isDocker = selectedType === 'docker' - const previousType = isDocker ? 'buildpacks' : 'docker' - const nextMode = { - ...(watch(`spec.mode.${previousType}`) as any), - path: isDocker ? './Dockerfile' : '', + {appsMissing && } + + + + + + +
+ Select build task + + + { + const isDocker = selectedType === 'docker' + const previousType = isDocker ? 'buildpacks' : 'docker' + const nextMode = { + ...(watch(`spec.mode.${previousType}`) as any), + path: isDocker ? './Dockerfile' : '', + } + + setValue(`spec.mode.${selectedType as 'docker' | 'buildpacks'}` as any, nextMode) + unregister(`spec.mode.${previousType}` as any) + }} + /> + + + Select code repository + + + + label='Repository' + loading={isLoadingCodeRepos} + options={filteredCodeRepos} + getOptionLabel={(codeRepo) => codeRepo.metadata.name} + placeholder='Select a repository' + value={filteredCodeRepos.find((cr) => cr?.spec?.repositoryUrl === repoField.value) || null} + onChange={(_e, repo) => { + repoField.onChange(repo?.spec?.repositoryUrl ?? '') + if (!repo) return + + const name = repo.metadata.name + const gitService = repo.spec.gitService + const isPrivate = repo.spec.private + const secret = repo.spec.secret + + if (!buildName) setValue('spec.imageName', name) + setValue(`spec.mode.${modeType}.revision` as any, undefined as any) + + setValue('spec.externalRepo', gitService !== 'gitea') + setRepoName(name) + setGitService(gitService) + + if (isPrivate) setValue('spec.secretName', secret) + else unregister('spec.secretName') + }} + errorText={(errors as any)?.spec?.mode?.[modeType]?.repoUrl?.message?.toString()} + disabled={!!buildName || appsMissing} + /> + + + label='Reference' + loading={isLoadingRepoBranches} + options={repoBranches || []} + getOptionLabel={(repoBranch) => repoBranch} + placeholder='Select a reference' + value={(revField.value as string) ?? ''} + onChange={(_e, branch) => { + revField.onChange(branch ?? '') + if (!buildName) setValue('spec.tag', branch ?? '') + }} + errorText={(errors as any)?.spec?.mode?.[modeType]?.revision?.message?.toString()} + disabled={appsMissing} + /> + + setValue(`spec.mode.${watch('spec.mode.type')}.path` as any, e.target.value)} + error={!!(errors as any)?.spec?.mode?.[`${watch('spec.mode.type')}`]?.path} + helperText={ + (errors as any)?.spec?.mode?.[`${watch('spec.mode.type')}`]?.path?.message?.toString() || + pathHelperText } + disabled={appsMissing} + /> + + + + + Image name and tag + + + setValue('spec.imageName', e.target.value)} + error={!!(errors as any)?.spec?.imageName} + helperText={(errors as any)?.spec?.imageName?.message?.toString()} + disabled={!!buildName || appsMissing} + /> - setValue(`spec.mode.${selectedType as 'docker' | 'buildpacks'}` as any, nextMode) - unregister(`spec.mode.${previousType}` as any) - }} - /> - - - Select code repository - - - - label='Repository' - loading={isLoadingCodeRepos} - options={filteredCodeRepos} - getOptionLabel={(codeRepo) => codeRepo.metadata.name} - placeholder='Select a repository' - value={filteredCodeRepos.find((cr) => cr?.spec?.repositoryUrl === repoField.value) || null} - onChange={(_e, repo) => { - repoField.onChange(repo?.spec?.repositoryUrl ?? '') - if (!repo) return - - const name = repo.metadata.name - const gitService = repo.spec.gitService - const isPrivate = repo.spec.private - const secret = repo.spec.secret - - if (!buildName) setValue('spec.imageName', name) - setValue(`spec.mode.${modeType}.revision` as any, undefined as any) - - setValue('spec.externalRepo', gitService !== 'gitea') - setRepoName(name) - setGitService(gitService) - - if (isPrivate) setValue('spec.secretName', secret) - else unregister('spec.secretName') - }} - errorText={(errors as any)?.spec?.mode?.[modeType]?.repoUrl?.message?.toString()} - disabled={!!buildName} + setValue('spec.tag', e.target.value)} + error={!!(errors as any)?.spec?.tag} + helperText={(errors as any)?.spec?.tag?.message?.toString()} + disabled={!!buildName || appsMissing} + /> + + + + {buildName + ? `Full repository name: harbor.${domainSuffix}/team-${teamId}/${buildData?.spec?.imageName}:${buildData?.spec?.tag}` + : `Full repository name: harbor.${domainSuffix}/team-${teamId}/${ + watch('spec.imageName') || '___' + }:${watch('spec.tag') || '___'}`} + + + + + - - label='Reference' - loading={isLoadingRepoBranches} - options={repoBranches || []} - getOptionLabel={(repoBranch) => repoBranch} - placeholder='Select a reference' - value={(revField.value as string) ?? ''} - onChange={(_e, branch) => { - revField.onChange(branch ?? '') - if (!buildName) setValue('spec.tag', branch ?? '') - }} - errorText={(errors as any)?.spec?.mode?.[modeType]?.revision?.message?.toString()} - /> + - setValue(`spec.mode.${watch('spec.mode.type')}.path` as any, e.target.value)} - error={!!(errors as any)?.spec?.mode?.[`${watch('spec.mode.type')}`]?.path} - helperText={ - (errors as any)?.spec?.mode?.[`${watch('spec.mode.type')}`]?.path?.message?.toString() || - pathHelperText - } - /> - - - - - Image name and tag - - - setValue('spec.imageName', e.target.value)} - error={!!(errors as any)?.spec?.imageName} - helperText={(errors as any)?.spec?.imageName?.message?.toString()} - disabled={!!buildName} - /> + Extra options - setValue('spec.tag', e.target.value)} - error={!!(errors as any)?.spec?.tag} - helperText={(errors as any)?.spec?.tag?.message?.toString()} - disabled={!!buildName} - /> - + + {appsEnabled?.gitea && gitService === 'gitea' && ( + + )} - - {buildName - ? `Full repository name: harbor.${domainSuffix}/team-${teamId}/${buildData?.spec?.imageName}:${buildData?.spec?.tag}` - : `Full repository name: harbor.${domainSuffix}/team-${teamId}/${watch('spec.imageName') || '___'}:${ - watch('spec.tag') || '___' - }`} - - - - - - - - - Extra options - - - {appsEnabled?.gitea && gitService === 'gitea' && ( - )} - - +
+ + {buildName && ( + del({ teamId, buildName })} + resourceName={watch('metadata.name')} + resourceType='build' + data-cy='button-delete-build' + sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} + loading={isLoadingDelete} + disabled={appsMissing || isLoadingDelete || isLoadingCreate || isLoadingUpdate} /> -
-
- - {buildName && ( - del({ teamId, buildName })} - resourceName={watch('metadata.name')} - resourceType='build' - data-cy='button-delete-build' - sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} - loading={isLoadingDelete} - disabled={isLoadingDelete || isLoadingCreate || isLoadingUpdate} - /> - )} - - - {buildName ? 'Save Changes' : 'Create Container Image'} - - -
+ )} + + + {buildName ? 'Save Changes' : 'Create Container Image'} + + + +
) diff --git a/src/pages/builds/overview/BuildsOverviewPage.tsx b/src/pages/builds/overview/BuildsOverviewPage.tsx index ee95d582..169fe630 100644 --- a/src/pages/builds/overview/BuildsOverviewPage.tsx +++ b/src/pages/builds/overview/BuildsOverviewPage.tsx @@ -7,13 +7,15 @@ import PaperLayout from 'layouts/Paper' import { useSession } from 'providers/Session' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Link, RouteComponentProps } from 'react-router-dom' +import { Link, RouteComponentProps, useHistory } from 'react-router-dom' import { useAppSelector } from 'redux/hooks' import { useGetAllAplBuildsQuery, useGetTeamAplBuildsQuery } from 'redux/otomiApi' import { getRole } from 'utils/data' import { Box, Typography, useTheme } from '@mui/material' import { useSocket } from 'providers/Socket' import CopyToClipboard from 'components/CopyToClipboard' +import MuiLink from 'components/MuiLink' +import useSettings from 'hooks/useSettings' import RLink from '../../../components/Link' interface Row { @@ -119,11 +121,15 @@ export default function BuildsOverviewPage({ const { t } = useTranslation() const { appsEnabled, + user, settings: { cluster: { domainSuffix }, }, } = useSession() + const { isPlatformAdmin } = user const { statuses } = useSocket() + const { onToggleView } = useSettings() + const history = useHistory() const { data: allBuilds, @@ -188,23 +194,44 @@ export default function BuildsOverviewPage({ }) } - const customButtonText = () => Create container image - const loading = isLoadingAllBuilds || isLoadingTeamBuilds const builds = teamId ? teamBuilds : allBuilds - const comp = !appsEnabled.harbor ? ( - + const appsMissing = !appsEnabled.tekton || !appsEnabled.harbor + + const handleAppsClick = (e: React.MouseEvent) => { + e.preventDefault() + + onToggleView() + + history.push('/apps/admin') + } + + const bannerMessage = isPlatformAdmin ? ( + <> + Container Images requires Tekton and Harbor to be enabled. Click{' '} + + here + {' '} + to enable them. + ) : ( - builds && ( + 'Admin needs to enable the Tekton and Harbor app to activate this feature.' + ) + + const comp = ( + <> + {appsMissing && } + Create container image} + createButtonDisabled={appsMissing} /> - ) + ) return