Skip to content

Commit

Permalink
feat(dashboard): Improve error handling for missing permissions when …
Browse files Browse the repository at this point in the history
…creating repositories
  • Loading branch information
framitdavid committed Jan 9, 2025
1 parent 907ab42 commit bc14a2f
Show file tree
Hide file tree
Showing 8 changed files with 47 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { type FormEvent, type ChangeEvent } from 'react';
import React, { type FormEvent, type ChangeEvent, useState } from 'react';
import classes from './NewApplicationForm.module.css';
import { StudioButton, StudioSpinner } from '@studio/components';
import { useTranslation } from 'react-i18next';
Expand All @@ -11,6 +11,7 @@ import { SelectedContextType } from 'dashboard/context/HeaderContext';
import { type NewAppForm } from '../../types/NewAppForm';
import { useCreateAppFormValidation } from './hooks/useCreateAppFormValidation';
import { Link } from 'react-router-dom';
import { useUserOrgPermissionQuery } from '../../hooks/queries/useUserOrgPermissionsQuery';

type CancelButton = {
onClick: () => void;
Expand Down Expand Up @@ -47,11 +48,14 @@ export const NewApplicationForm = ({
const { t } = useTranslation();
const selectedContext = useSelectedContext();
const { validateRepoOwnerName, validateRepoName } = useCreateAppFormValidation();

const defaultSelectedOrgOrUser: string =
selectedContext === SelectedContextType.Self || selectedContext === SelectedContextType.All
? user.login
: selectedContext;
const [currentSelectedOrg, setCurrentSelectedOrg] = useState<string>(defaultSelectedOrgOrUser);
const { data: userOrgPermission, isFetching } = useUserOrgPermissionQuery(currentSelectedOrg, {
enabled: Boolean(currentSelectedOrg),
});

const validateTextValue = (event: ChangeEvent<HTMLInputElement>) => {
const { errorMessage: repoNameErrorMessage, isValid: isRepoNameValid } = validateRepoName(
Expand Down Expand Up @@ -96,14 +100,22 @@ export const NewApplicationForm = ({
return isOrgValid && isRepoNameValid;
};

const createRepoAccessError =
!userOrgPermission?.canCreateOrgRepo && !isFetching
? t('dashboard.missing_service_owner_rights_error_message')
: '';

const hasCreateRepoAccessError = Boolean(createRepoAccessError);

return (
<form onSubmit={handleSubmit} className={classes.form}>
<ServiceOwnerSelector
name='org'
user={user}
organizations={organizations}
errorMessage={formError.org}
errorMessage={formError.org || createRepoAccessError}
selectedOrgOrUser={defaultSelectedOrgOrUser}
onChange={setCurrentSelectedOrg}
/>
<RepoNameInput
name='repoName'
Expand All @@ -115,7 +127,7 @@ export const NewApplicationForm = ({
<StudioSpinner showSpinnerTitle spinnerTitle={t('dashboard.creating_your_service')} />
) : (
<>
<StudioButton type='submit' variant='primary'>
<StudioButton type='submit' variant='primary' disabled={hasCreateRepoAccessError}>
{submitButtonText}
</StudioButton>
<CancelComponent actionableElement={actionableElement} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type ServiceOwnerSelectorProps = {
organizations: Organization[];
errorMessage?: string;
name?: string;
onChange?: (org: string) => void;
};

export const ServiceOwnerSelector = ({
Expand All @@ -18,6 +19,7 @@ export const ServiceOwnerSelector = ({
organizations,
errorMessage,
name,
onChange,
}: ServiceOwnerSelectorProps) => {
const { t } = useTranslation();
const serviceOwnerId: string = useId();
Expand All @@ -38,6 +40,7 @@ export const ServiceOwnerSelector = ({
name={name}
id={serviceOwnerId}
defaultValue={defaultValue}
onChange={(event) => onChange(event.target.value)}
>
{selectableOptions.map(({ value, label }) => (
<option key={value} value={value}>
Expand Down
20 changes: 20 additions & 0 deletions frontend/dashboard/hooks/queries/useUserOrgPermissionsQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { QueryKey } from 'app-shared/types/QueryKey';

type UserOrgPermission = {
canCreateOrgRepo: boolean;
};

export const useUserOrgPermissionQuery = (
org: string,
options?: { enabled: boolean },
): UseQueryResult<UserOrgPermission> => {
const { getUserOrgPermissions } = useServicesContext();
return useQuery({
queryKey: [QueryKey.UserOrgPermissions, org],
queryFn: () => getUserOrgPermissions(org),
...options,
});
};
1 change: 1 addition & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
"dashboard.loading": "Laster inn dashboardet",
"dashboard.loading_resource_list": "Laster inn ressursliste",
"dashboard.make_copy": "Lag kopi",
"dashboard.missing_service_owner_rights_error_message": "Du mangler tilgang til å opprette en ny applikasjon i valgt organisasjon.",
"dashboard.my_apps": "Mine apper",
"dashboard.my_data_models": "Mine datamodeller",
"dashboard.my_resources": "Mine ressurser",
Expand Down
3 changes: 3 additions & 0 deletions frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export const appMetadataAttachmentPath = (org, app) => `${basePath}/${org}/${app
// App version
export const appVersionPath = (org, app) => `${basePath}/${org}/${app}/app-development/app-version`; // Get

// UserOrgPermissions
export const userOrgPermissionsPath = (org) => `${basePath}/user/org-permissions/${org}`;

// Config
export const serviceConfigPath = (org, app) => `${basePath}/${org}/${app}/config`; // Get, Post

Expand Down
2 changes: 2 additions & 0 deletions frontend/packages/shared/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
resourceAccessPackageServicesPath,
optionListPath,
optionListReferencesPath,
userOrgPermissionsPath,
} from './paths';

import type { AppReleasesResponse, DataModelMetadataResponse, SearchRepoFilterParams, SearchRepositoryResponse } from 'app-shared/types/api';
Expand Down Expand Up @@ -134,6 +135,7 @@ export const getTextLanguages = (owner: string, app: string): Promise<string[]>
export const getTextResources = (owner: string, app: string, lang: string) => get<ITextResourcesWithLanguage>(textResourcesPath(owner, app, lang));
export const getUser = () => get<User>(userCurrentPath());
export const getWidgetSettings = (owner: string, app: string) => get<WidgetSettingsResponse | null>(widgetSettingsPath(owner, app));
export const getUserOrgPermissions = (org: string) => get(userOrgPermissionsPath(org));
export const searchRepos = (filter: SearchRepoFilterParams) => get<SearchRepositoryResponse>(`${repoSearchPath()}${buildQueryParams(filter)}`);
export const validateImageFromExternalUrl = (owner: string, app: string, url: string) => get<ExternalImageUrlValidationResponse>(validateImageFromExternalUrlPath(owner, app, url));

Expand Down
1 change: 1 addition & 0 deletions frontend/packages/shared/src/mocks/queriesMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export const queriesMock: ServicesContextProps = {
validateImageFromExternalUrl: jest
.fn()
.mockImplementation(() => Promise.resolve<ExternalImageUrlValidationResponse>('Ok')),
getUserOrgPermissions: jest.fn().mockImplementation(() => Promise.resolve({})),

// Queries - Settings modal
getAppConfig: jest.fn().mockImplementation(() => Promise.resolve<AppConfig>(appConfig)),
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/shared/src/types/QueryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export enum QueryKey {
IsLoggedInWithAnsattporten = 'IsLoggedInWithAnsattporten',
AppScopes = 'AppScopes',
SelectedAppScopes = 'SelectedAppScopes',
UserOrgPermissions = 'UserOrgPermissions',

// Resourceadm
ResourceList = 'ResourceList',
Expand Down

0 comments on commit bc14a2f

Please sign in to comment.