From 727704086327622b55865a2a1feb3bfc193fe1b9 Mon Sep 17 00:00:00 2001 From: WilliamThorenfeldt <133344438+WilliamThorenfeldt@users.noreply.github.com> Date: Wed, 25 Oct 2023 08:09:17 +0200 Subject: [PATCH] Feature/10670 import service from altinn 2 (#11384) * adding basic frontend logic * spll check * typofix * Fixed spelling * Adding style * Fixed ttd handling * Adding handling for service * fixing code * Fixed cache key * Changed to post for import * Trying to set up import * Trying to fix undefined * fixing undefined * updating post * Cleaning up cod * Adding tests * fixing queriesMock * rollback useGetResourceList * dotnet format * Fixed method * Fixing modal z index and SetupTab * Fixing feedback fromPR * Adding ServerCode enum * Feedback. Added util fo rcommmon org code --------- Co-authored-by: Rune T. Larsen --- .../Controllers/ResourceAdminController.cs | 18 +- backend/src/Designer/Designer.csproj | 1 + backend/src/Designer/Helpers/OrgUtil.cs | 12 ++ .../Altinn2Metadata/Altinn2MetadataClient.cs | 4 +- .../ImportResourceTests.cs | 2 +- .../pages/CreateService/CreateService.tsx | 3 +- frontend/language/src/nb.json | 2 + frontend/packages/shared/src/api/mutations.ts | 2 + frontend/packages/shared/src/api/paths.js | 2 + frontend/packages/shared/src/api/queries.ts | 3 + .../packages/shared/src/enums/ServerCodes.ts | 3 + .../packages/shared/src/mocks/queriesMock.ts | 2 + .../shared/src/types/Altinn2LinkService.ts | 5 + .../packages/shared/src/types/QueryKey.ts | 2 + .../ImportResourceModal.module.css | 7 - .../ImportResourceModal.test.tsx | 122 ++++++++++-- .../ImportResourceModal.tsx | 177 +++++------------- .../ResourceContent.module.css | 6 + .../ResourceContent/ResourceContent.test.tsx | 69 +++++++ .../ResourceContent/ResourceContent.tsx | 94 ++++++++++ .../ServiceContent/ResourceContent/index.ts | 1 + .../ServiceContent/ServiceContent.module.css | 5 + .../ServiceContent/ServiceContent.test.tsx | 143 ++++++++++++++ .../ServiceContent/ServiceContent.tsx | 116 ++++++++++++ .../ServiceContent/index.ts | 1 + .../NewResourceModal/NewResourceModal.tsx | 38 ++-- .../ResourceNameAndId/ResourceNameAndId.tsx | 40 +--- frontend/resourceadm/hooks/mutations/index.ts | 1 + .../useImportResourceFromAltinn2Mutation.ts | 29 +++ frontend/resourceadm/hooks/queries/index.ts | 1 + .../queries/useGetAltinn2LinkServicesQuery.ts | 25 +++ frontend/resourceadm/types/global.ts | 4 - .../resourceadm/utils/mapperUtils/index.ts | 2 +- .../utils/mapperUtils/mapperUtils.test.ts | 27 +++ .../utils/mapperUtils/mapperUtils.ts | 18 +- .../resourceadm/utils/stringUtils/index.ts | 1 + .../utils/stringUtils/stringUtils.test.ts | 13 ++ .../utils/stringUtils/stringUtils.ts | 8 + 38 files changed, 784 insertions(+), 225 deletions(-) create mode 100644 backend/src/Designer/Helpers/OrgUtil.cs create mode 100644 frontend/packages/shared/src/enums/ServerCodes.ts create mode 100644 frontend/packages/shared/src/types/Altinn2LinkService.ts create mode 100644 frontend/resourceadm/components/ImportResourceModal/ServiceContent/ResourceContent/ResourceContent.module.css create mode 100644 frontend/resourceadm/components/ImportResourceModal/ServiceContent/ResourceContent/ResourceContent.test.tsx create mode 100644 frontend/resourceadm/components/ImportResourceModal/ServiceContent/ResourceContent/ResourceContent.tsx create mode 100644 frontend/resourceadm/components/ImportResourceModal/ServiceContent/ResourceContent/index.ts create mode 100644 frontend/resourceadm/components/ImportResourceModal/ServiceContent/ServiceContent.module.css create mode 100644 frontend/resourceadm/components/ImportResourceModal/ServiceContent/ServiceContent.test.tsx create mode 100644 frontend/resourceadm/components/ImportResourceModal/ServiceContent/ServiceContent.tsx create mode 100644 frontend/resourceadm/components/ImportResourceModal/ServiceContent/index.ts create mode 100644 frontend/resourceadm/hooks/mutations/useImportResourceFromAltinn2Mutation.ts create mode 100644 frontend/resourceadm/hooks/queries/useGetAltinn2LinkServicesQuery.ts create mode 100644 frontend/resourceadm/utils/mapperUtils/mapperUtils.test.ts create mode 100644 frontend/resourceadm/utils/stringUtils/index.ts create mode 100644 frontend/resourceadm/utils/stringUtils/stringUtils.test.ts create mode 100644 frontend/resourceadm/utils/stringUtils/stringUtils.ts diff --git a/backend/src/Designer/Controllers/ResourceAdminController.cs b/backend/src/Designer/Controllers/ResourceAdminController.cs index a051012cdd2..84d8704e0b5 100644 --- a/backend/src/Designer/Controllers/ResourceAdminController.cs +++ b/backend/src/Designer/Controllers/ResourceAdminController.cs @@ -183,7 +183,7 @@ public async Task> AddResource(string org, [FromBo return _repository.AddServiceResource(org, resource); } - [HttpGet] + [HttpPost] [Route("designer/api/{org}/resources/importresource/{serviceCode}/{serviceEdition}/{environment}")] public async Task ImportResource(string org, string serviceCode, int serviceEdition, string environment) { @@ -260,19 +260,27 @@ public async Task>> GetEuroVoc(CancellationToken [HttpGet] [Route("designer/api/{org}/resources/altinn2linkservices/{environment}")] - public async Task>> GetAltinn2LinkServices(string org, string enviroment) + public async Task>> GetAltinn2LinkServices(string org, string environment) { - string cacheKey = "availablelinkservices:" + org; + string cacheKey = "availablelinkservices:" + org + environment; if (!_memoryCache.TryGetValue(cacheKey, out List linkServices)) { - List unfiltered = await _altinn2MetadataClient.AvailableServices(1044, enviroment); + List unfiltered = await _altinn2MetadataClient.AvailableServices(1044, environment); var cacheEntryOptions = new MemoryCacheEntryOptions() .SetPriority(CacheItemPriority.High) .SetAbsoluteExpiration(new TimeSpan(0, _cacheSettings.DataNorgeApiCacheTimeout, 0)); - linkServices = unfiltered.Where(a => a.ServiceType.Equals(ServiceType.Link) && a.ServiceOwnerCode.ToLower().Equals(org.ToLower())).ToList(); + if (OrgUtil.IsTestEnv(org)) + { + linkServices = unfiltered.Where(a => a.ServiceType.Equals(ServiceType.Link) && (a.ServiceOwnerCode.ToLower().Equals(org.ToLower()) || a.ServiceOwnerCode.ToLower().Equals("acn"))).ToList(); + } + else + { + linkServices = unfiltered.Where(a => a.ServiceType.Equals(ServiceType.Link) && a.ServiceOwnerCode.ToLower().Equals(org.ToLower())).ToList(); + } + _memoryCache.Set(cacheKey, linkServices, cacheEntryOptions); } diff --git a/backend/src/Designer/Designer.csproj b/backend/src/Designer/Designer.csproj index 08dd310072a..59584c85fb3 100644 --- a/backend/src/Designer/Designer.csproj +++ b/backend/src/Designer/Designer.csproj @@ -76,6 +76,7 @@ + diff --git a/backend/src/Designer/Helpers/OrgUtil.cs b/backend/src/Designer/Helpers/OrgUtil.cs new file mode 100644 index 00000000000..95d6df18489 --- /dev/null +++ b/backend/src/Designer/Helpers/OrgUtil.cs @@ -0,0 +1,12 @@ +using System; + +namespace Altinn.Studio.Designer.Helpers +{ + public static class OrgUtil + { + public static bool IsTestEnv(string org) + { + return string.Equals(org, "ttd", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/backend/src/Designer/TypedHttpClients/Altinn2Metadata/Altinn2MetadataClient.cs b/backend/src/Designer/TypedHttpClients/Altinn2Metadata/Altinn2MetadataClient.cs index 903b7ba7670..24a135b8c98 100644 --- a/backend/src/Designer/TypedHttpClients/Altinn2Metadata/Altinn2MetadataClient.cs +++ b/backend/src/Designer/TypedHttpClients/Altinn2Metadata/Altinn2MetadataClient.cs @@ -55,7 +55,7 @@ public async Task> AvailableServices(int languageId, stri { List? availableServices = null; string bridgeBaseUrl = GetSblBridgeUrl(environment); - string availabbleServicePath = $"h{bridgeBaseUrl}metadata/api/availableServices?languageID={languageId}&appTypesToInclude=0&includeExpired=false"; + string availabbleServicePath = $"{bridgeBaseUrl}metadata/api/availableServices?languageID={languageId}&appTypesToInclude=0&includeExpired=false"; HttpResponseMessage response = await _httpClient.GetAsync(availabbleServicePath); @@ -70,7 +70,7 @@ public async Task> AvailableServices(int languageId, stri private string GetSblBridgeUrl(string environment) { - if (!_rrs.TryGetValue(environment, out ResourceRegistryEnvironmentSettings envSettings)) + if (!_rrs.TryGetValue(environment.ToLower(), out ResourceRegistryEnvironmentSettings envSettings)) { throw new ArgumentException($"Invalid environment. Missing environment config for {environment}"); } diff --git a/backend/tests/Designer.Tests/Controllers/ResourceAdminController/ImportResourceTests.cs b/backend/tests/Designer.Tests/Controllers/ResourceAdminController/ImportResourceTests.cs index b5bb00a9c92..bc99b979117 100644 --- a/backend/tests/Designer.Tests/Controllers/ResourceAdminController/ImportResourceTests.cs +++ b/backend/tests/Designer.Tests/Controllers/ResourceAdminController/ImportResourceTests.cs @@ -22,7 +22,7 @@ public async Task ExportAltinn2Resource() { // Arrange string uri = $"designer/api/ttd/resources/importresource/4485/4444/at23"; - using (HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri)) + using (HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, uri)) { ServiceResource serviceResource = new ServiceResource() { diff --git a/frontend/dashboard/pages/CreateService/CreateService.tsx b/frontend/dashboard/pages/CreateService/CreateService.tsx index 2ee93c6f6f5..d98b582d20f 100644 --- a/frontend/dashboard/pages/CreateService/CreateService.tsx +++ b/frontend/dashboard/pages/CreateService/CreateService.tsx @@ -16,6 +16,7 @@ import { SelectedContextType } from 'app-shared/navigation/main-header/Header'; import { useSelectedContext } from 'dashboard/hooks/useSelectedContext'; import { useNavigate } from 'react-router-dom'; import { AxiosError } from 'axios'; +import { ServerCodes } from 'app-shared/enums/ServerCodes'; enum PageState { Idle = 'Idle', @@ -111,7 +112,7 @@ export const CreateService = ({ user, organizations }: CreateServiceProps): JSX. ); }, onError: (error: { response: { status: number } }) => { - if (error.response.status === 409) { + if (error.response.status === ServerCodes.Conflict) { setRepoErrorMessage(t('dashboard.app_already_exists')); } diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 9753bc6ed6c..b0429cc3a70 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -774,6 +774,8 @@ "resourceadm.deploy_version_upload_button": "Last opp dine endringer", "resourceadm.error_back_to_dashboard": "Gå tilbake til dashbord", "resourceadm.error_page_text": "Du har nådd en ugyldig adresse", + "resourceadm.import_resource_empty_list": "Det finnes ingen servicer i {{env}}-miljøet", + "resourceadm.import_resource_spinner": "Importerer ressursen", "resourceadm.left_nav_bar_about": "Om ressursen", "resourceadm.left_nav_bar_back": "Tilbake til dashbord", "resourceadm.left_nav_bar_back_icon": "Tilbake til dashbord", diff --git a/frontend/packages/shared/src/api/mutations.ts b/frontend/packages/shared/src/api/mutations.ts index 4b94ac0a2ec..48b8cfae630 100644 --- a/frontend/packages/shared/src/api/mutations.ts +++ b/frontend/packages/shared/src/api/mutations.ts @@ -29,6 +29,7 @@ import { publishResourcePath, appMetadataPath, serviceConfigPath, + importResourceFromAltinn2Path, } from 'app-shared/api/paths'; import { AddLanguagePayload } from 'app-shared/types/api/AddLanguagePayload'; import { AddRepoParams } from 'app-shared/types/api'; @@ -94,3 +95,4 @@ export const updatePolicy = (org: string, repo: string, id: string, payload: Pol export const createResource = (org: string, payload: NewResource) => post(resourceCreatePath(org), payload); export const updateResource = (org: string, repo: string, payload: Resource) => put(resourceEditPath(org, repo), payload); export const publishResource = (org: string, repo: string, id: string, env: string) => post(publishResourcePath(org, repo, id, env), { headers: { 'Content-Type': 'application/json' } }); +export const importResourceFromAltinn2 = (org: string, environment: string, serviceCode: string, serviceEdition: string) => post(importResourceFromAltinn2Path(org, environment, serviceCode, serviceEdition)); diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index d5af6b79547..3737125bea1 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -136,6 +136,8 @@ export const resourceEditPath = (org, id) => `${basePath}/${org}/resources/updat export const resourceValidatePolicyPath = (org, repo, id) => `${basePath}/${org}/${repo}/policy/validate/${id}`; // Get export const resourceValidateResourcePath = (org, repo, id) => `${basePath}/${org}/resources/validate/${repo}/${id}`; // Get export const publishResourcePath = (org, repo, id, env) => `${basePath}/${org}/resources/publish/${repo}/${id}?env=${env}`; // Get +export const altinn2LinkServicesPath = (org, env) => `${basePath}/${org}/resources/altinn2linkservices/${env}`; // Get +export const importResourceFromAltinn2Path = (org, env, serviceCode, serviceEdition) => `${basePath}/${org}/resources/importresource/${serviceCode}/${serviceEdition}/${env}`; // Post // Process Editor export const processEditorPath = (org, repo) => `${basePath}/${org}/${repo}/process-modelling/process-definition`; diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts index b725b13f547..db7b0380fd6 100644 --- a/frontend/packages/shared/src/api/queries.ts +++ b/frontend/packages/shared/src/api/queries.ts @@ -1,5 +1,6 @@ import { get, put } from 'app-shared/utils/networking'; import { + altinn2LinkServicesPath, appMetadataPath, appPolicyPath, branchStatusPath, @@ -64,6 +65,7 @@ import type { Resource, ResourceListItem, ResourceVersionStatus, Validation } fr import type { AppConfig } from 'app-shared/types/AppConfig'; import type { Commit } from 'app-shared/types/Commit'; import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata'; +import { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService'; export const getAppReleases = (owner: string, app: string) => get(releasesPath(owner, app, 'Descending')); export const getBranchStatus = (owner: string, app: string, branch: string) => get(branchStatusPath(owner, app, branch)); @@ -115,6 +117,7 @@ export const getResourceList = (org: string) => get(resource export const getResource = (org: string, repo: string, id: string) => get(resourceSinglePath(org, repo, id)); export const getValidatePolicy = (org: string, repo: string, id: string) => get(resourceValidatePolicyPath(org, repo, id)); export const getValidateResource = (org: string, repo: string, id: string) => get(resourceValidateResourcePath(org, repo, id)); +export const getAltinn2LinkServices = (org: string, environment: string) => get(altinn2LinkServicesPath(org, environment)); // ProcessEditor export const getBpmnFile = (org: string, app: string) => get(processEditorPath(org, app)); diff --git a/frontend/packages/shared/src/enums/ServerCodes.ts b/frontend/packages/shared/src/enums/ServerCodes.ts new file mode 100644 index 00000000000..2e4042518f2 --- /dev/null +++ b/frontend/packages/shared/src/enums/ServerCodes.ts @@ -0,0 +1,3 @@ +export enum ServerCodes { + Conflict = 409, +} diff --git a/frontend/packages/shared/src/mocks/queriesMock.ts b/frontend/packages/shared/src/mocks/queriesMock.ts index f2ac507b724..3edb66ba8fc 100644 --- a/frontend/packages/shared/src/mocks/queriesMock.ts +++ b/frontend/packages/shared/src/mocks/queriesMock.ts @@ -88,4 +88,6 @@ export const queriesMock: ServicesContextProps = { updateAppConfig: jest.fn(), getRepoInitialCommit: jest.fn(), publishResource: jest.fn(), + getAltinn2LinkServices: jest.fn(), + importResourceFromAltinn2: jest.fn(), }; diff --git a/frontend/packages/shared/src/types/Altinn2LinkService.ts b/frontend/packages/shared/src/types/Altinn2LinkService.ts new file mode 100644 index 00000000000..dec93277f47 --- /dev/null +++ b/frontend/packages/shared/src/types/Altinn2LinkService.ts @@ -0,0 +1,5 @@ +export interface Altinn2LinkService { + serviceName: string; + externalServiceCode: string; + externalServiceEditionCode: string; +} diff --git a/frontend/packages/shared/src/types/QueryKey.ts b/frontend/packages/shared/src/types/QueryKey.ts index d75dbf7c370..467252e3592 100644 --- a/frontend/packages/shared/src/types/QueryKey.ts +++ b/frontend/packages/shared/src/types/QueryKey.ts @@ -50,4 +50,6 @@ export enum QueryKey { ValidatePolicy = 'ValidatePolicy', ValidateResource = 'ValidateResource', PublishResource = 'PublishResource', + Altinn2Services = 'Altinn2Services', + ImportAltinn2Resource = 'ImportAltinn2Resource', } diff --git a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.module.css b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.module.css index 875df999cf7..528b0acb986 100644 --- a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.module.css +++ b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.module.css @@ -18,13 +18,6 @@ margin-left: 10px; } -.contentDivider { - border: solid 1px #d6d6d6; - margin-top: 25px; - margin-bottom: 15px; - width: 80%; -} - .contentWidth { max-width: 600px; } diff --git a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx index 6a8f932a4ae..82874f8388e 100644 --- a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx +++ b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx @@ -1,45 +1,89 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render as rtlRender, screen, waitForElementToBeRemoved } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { ImportResourceModal, ImportResourceModalProps } from './ImportResourceModal'; // Update the import path +import { ImportResourceModal, ImportResourceModalProps } from './ImportResourceModal'; import { textMock } from '../../../testing/mocks/i18nMock'; import { act } from 'react-dom/test-utils'; +import { MemoryRouter } from 'react-router-dom'; +import { ServicesContextProps, ServicesContextProvider } from 'app-shared/contexts/ServicesContext'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService'; +import { useImportResourceFromAltinn2Mutation } from 'resourceadm/hooks/mutations'; +import { UseMutationResult } from '@tanstack/react-query'; +import { Resource } from 'app-shared/types/ResourceAdm'; -describe('ImportResourceModal', () => { - const mockOnClose = jest.fn(); +const mockAltinn2LinkService: Altinn2LinkService = { + externalServiceCode: 'code1', + externalServiceEditionCode: 'edition1', + serviceName: 'TestService', +}; +const mockAltinn2LinkServices: Altinn2LinkService[] = [mockAltinn2LinkService]; +const mockOption: string = `${mockAltinn2LinkService.externalServiceCode}-${mockAltinn2LinkService.externalServiceEditionCode}-${mockAltinn2LinkService.serviceName}`; - const defaultProps: ImportResourceModalProps = { - isOpen: true, - onClose: mockOnClose, - }; +const mockOnClose = jest.fn(); +const getAltinn2LinkServices = jest.fn().mockImplementation(() => Promise.resolve({})); + +jest.mock('../../hooks/mutations/useImportResourceFromAltinn2Mutation'); +const importResourceFromAltinn2 = jest.fn(); +const mockImportResourceFromAltinn2 = useImportResourceFromAltinn2Mutation as jest.MockedFunction< + typeof useImportResourceFromAltinn2Mutation +>; +mockImportResourceFromAltinn2.mockReturnValue({ + mutate: importResourceFromAltinn2, +} as unknown as UseMutationResult< + Resource, + unknown, + { + environment: string; + serviceCode: string; + serviceEdition: string; + }, + unknown +>); + +const defaultProps: ImportResourceModalProps = { + isOpen: true, + onClose: mockOnClose, +}; + +describe('ImportResourceModal', () => { + afterEach(jest.clearAllMocks); it('selects environment and service, then checks if import button exists', async () => { const user = userEvent.setup(); - - render(); + render(); const importButtonText = textMock('resourceadm.dashboard_import_modal_import_button'); const importButton = screen.queryByRole('button', { name: importButtonText }); expect(importButton).not.toBeInTheDocument(); - const [, environmentSelect] = screen.getAllByLabelText(textMock('resourceadm.dashboard_import_modal_select_env')); + const [, environmentSelect] = screen.getAllByLabelText( + textMock('resourceadm.dashboard_import_modal_select_env'), + ); await act(() => user.click(environmentSelect)); - await act(() => user.click(screen.getByRole('option', { name: 'AT21' }))) + await act(() => user.click(screen.getByRole('option', { name: 'AT21' }))); expect(environmentSelect).toHaveValue('AT21'); expect(importButton).not.toBeInTheDocument(); - const [, serviceSelect] = screen.getAllByLabelText(textMock('resourceadm.dashboard_import_modal_select_service')); + await waitForElementToBeRemoved(() => + screen.queryByTitle(textMock('resourceadm.import_resource_spinner')), + ); + + const [, serviceSelect] = screen.getAllByLabelText( + textMock('resourceadm.dashboard_import_modal_select_service'), + ); await act(() => user.click(serviceSelect)); - await act(() => user.click(screen.getByRole('option', { name: 'Service1' }))) + await act(() => user.click(screen.getByRole('option', { name: mockOption }))); - expect(serviceSelect).toHaveValue('Service1'); + expect(serviceSelect).toHaveValue(mockOption); expect(screen.getByRole('button', { name: importButtonText })).toBeInTheDocument(); }); it('calls onClose function when close button is clicked', async () => { const user = userEvent.setup(); - render(); + render(); const closeButton = screen.getByRole('button', { name: textMock('general.cancel') }); await act(() => user.click(closeButton)); @@ -48,8 +92,52 @@ describe('ImportResourceModal', () => { }); it('should be closed by default', () => { - render( {}} />); + render({ isOpen: false }); + const closeButton = screen.queryByRole('button', { name: textMock('general.cancel') }); expect(closeButton).not.toBeInTheDocument(); }); + + it('calls import resource from Altinn 2 when import is clicked', async () => { + const user = userEvent.setup(); + render(); + + const [, environmentSelect] = screen.getAllByLabelText( + textMock('resourceadm.dashboard_import_modal_select_env'), + ); + await act(() => user.click(environmentSelect)); + await act(() => user.click(screen.getByRole('option', { name: 'AT21' }))); + await waitForElementToBeRemoved(() => + screen.queryByTitle(textMock('resourceadm.import_resource_spinner')), + ); + const [, serviceSelect] = screen.getAllByLabelText( + textMock('resourceadm.dashboard_import_modal_select_service'), + ); + await act(() => user.click(serviceSelect)); + await act(() => user.click(screen.getByRole('option', { name: mockOption }))); + + const importButton = screen.getByRole('button', { + name: textMock('resourceadm.dashboard_import_modal_import_button'), + }); + await act(() => user.click(importButton)); + + expect(importResourceFromAltinn2).toHaveBeenCalledTimes(1); + }); }); + +const render = (props: Partial = {}) => { + getAltinn2LinkServices.mockImplementation(() => Promise.resolve(mockAltinn2LinkServices)); + + const allQueries: ServicesContextProps = { + ...queriesMock, + getAltinn2LinkServices, + }; + + return rtlRender( + + + + + , + ); +}; diff --git a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx index ec3c6303472..c29bc929a28 100644 --- a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx +++ b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx @@ -2,33 +2,21 @@ import React, { useState } from 'react'; import classes from './ImportResourceModal.module.css'; import { Modal } from '../Modal'; import { Button, Select } from '@digdir/design-system-react'; -import { ResourceNameAndId } from '../ResourceNameAndId'; import { useTranslation } from 'react-i18next'; -import { EnvironmentType, ServiceType } from 'resourceadm/types/global'; - -const dummyServices: ServiceType[] = [ - { name: 'Service1' }, - { name: 'Service2' }, - { name: 'Service3' }, - { name: 'Service4' }, - { name: 'Service5' }, - { name: 'Service6' }, - { name: 'Service7' }, - { name: 'Service8' }, - { name: 'Service9' }, -]; +import { EnvironmentType } from 'resourceadm/types/global'; +import { useNavigate, useParams } from 'react-router-dom'; +import { ServiceContent } from './ServiceContent'; +import { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService'; +import { useImportResourceFromAltinn2Mutation } from 'resourceadm/hooks/mutations'; +import { Resource } from 'app-shared/types/ResourceAdm'; +import { getResourcePageURL } from 'resourceadm/utils/urlUtils'; +import { AxiosError } from 'axios'; +import { ServerCodes } from 'app-shared/enums/ServerCodes'; const environmentOptions = ['AT21', 'AT22', 'AT23', 'AT24', 'TT02', 'PROD']; export type ImportResourceModalProps = { - /** - * Boolean for if the modal is open - */ isOpen: boolean; - /** - * Function to handle close - * @returns void - */ onClose: () => void; }; @@ -52,12 +40,18 @@ export const ImportResourceModal = ({ }: ImportResourceModalProps): React.ReactNode => { const { t } = useTranslation(); + const { selectedContext } = useParams(); + const repo = `${selectedContext}-resources`; + + const navigate = useNavigate(); + const [selectedEnv, setSelectedEnv] = useState(); - const [selectedService, setSelectedService] = useState(); - const [id, setId] = useState(''); - const [title, setTitle] = useState(''); - const [editIdFieldOpen, setEditIdFieldOpen] = useState(false); - const [bothFieldsHaveSameValue, setBothFieldsHaveSameValue] = useState(true); + const [selectedService, setSelectedService] = useState(); + + const [resourceIdExists, setResourceIdExists] = useState(false); + + const { mutate: importResourceFromAltinn2Mutation } = + useImportResourceFromAltinn2Mutation(selectedContext); /** * Reset fields on close @@ -65,111 +59,29 @@ export const ImportResourceModal = ({ const handleClose = () => { onClose(); setSelectedEnv(undefined); - setSelectedService(undefined); - setId(''); - setTitle(''); - setEditIdFieldOpen(false); - }; - - /** - * Replaces the spaces in the value typed with '-'. - */ - const handleIDInput = (val: string) => { - setId(val.replace(/\s/g, '-')); }; /** - * Updates the value of the title. If the edit field is not open, - * then it updates the ID to the same as the title. - * - * @param val the title value typed + * Import the resource from Altinn 2, and navigate to about page on success */ - const handleEditTitle = (val: string) => { - if (!editIdFieldOpen && bothFieldsHaveSameValue) { - setId(val.replace(/\s/g, '-')); - } - setTitle(val); - }; - - /** - * Handles the click of the edit button. If we click the edit button - * so that it closes the edit field, the id is set to the title. - * - * @param isOpened the value of the button when it is pressed - */ - const handleClickEditButton = (isOpened: boolean, isSave: boolean) => { - setEditIdFieldOpen(isOpened); - - if (isSave) { - setBothFieldsHaveSameValue(false); - } else { - if (!isOpened) { - setBothFieldsHaveSameValue(true); - // If we stop editing, set the ID to the title - if (title !== id) setId(title.replace(/\s/g, '-')); - } - } - }; - - const handleSelectService = (s: string) => { - setSelectedService(s); - handleEditTitle(s); - }; - - /** - * Display loading (todo), the service, or nothing based on the - * state of the selected environment - */ - const displayService = () => { - // If environment loading, display loading, else do below - if (selectedEnv) { - return ( -
- + {selectedService && ( + + )} +
+ ); + } + } +}; diff --git a/frontend/resourceadm/components/ImportResourceModal/ServiceContent/index.ts b/frontend/resourceadm/components/ImportResourceModal/ServiceContent/index.ts new file mode 100644 index 00000000000..caa9a49e3ae --- /dev/null +++ b/frontend/resourceadm/components/ImportResourceModal/ServiceContent/index.ts @@ -0,0 +1 @@ +export { ServiceContent } from './ServiceContent'; diff --git a/frontend/resourceadm/components/NewResourceModal/NewResourceModal.tsx b/frontend/resourceadm/components/NewResourceModal/NewResourceModal.tsx index 1a50ec1546a..962392db0e3 100644 --- a/frontend/resourceadm/components/NewResourceModal/NewResourceModal.tsx +++ b/frontend/resourceadm/components/NewResourceModal/NewResourceModal.tsx @@ -8,16 +8,11 @@ import { useNavigate, useParams } from 'react-router-dom'; import type { NewResource } from 'app-shared/types/ResourceAdm'; import { getResourcePageURL } from 'resourceadm/utils/urlUtils'; import { useTranslation } from 'react-i18next'; +import { replaceWhiteSpaceWithHyphens } from 'resourceadm/utils/stringUtils'; +import { ServerCodes } from 'app-shared/enums/ServerCodes'; export type NewResourceModalProps = { - /** - * Boolean for if the modal is open - */ isOpen: boolean; - /** - * Function to handle close - * @returns void - */ onClose: () => void; }; @@ -64,7 +59,7 @@ export const NewResourceModal = ({ isOpen, onClose }: NewResourceModalProps): Re onSuccess: () => navigate(getResourcePageURL(selectedContext, repo, idAndTitle.identifier, 'about')), onError: (error: any) => { - if (error.response.status === 409) { + if (error.response.status === ServerCodes.Conflict) { setResourceIdExists(true); setEditIdFieldOpen(true); } @@ -76,7 +71,7 @@ export const NewResourceModal = ({ isOpen, onClose }: NewResourceModalProps): Re * Replaces the spaces in the value typed with '-'. */ const handleIDInput = (val: string) => { - setId(val.replace(/\s/g, '-')); + setId(replaceWhiteSpaceWithHyphens(val)); setResourceIdExists(false); }; @@ -88,7 +83,7 @@ export const NewResourceModal = ({ isOpen, onClose }: NewResourceModalProps): Re */ const handleEditTitle = (val: string) => { if (!editIdFieldOpen && bothFieldsHaveSameValue) { - setId(val.replace(/\s/g, '-')); + setId(replaceWhiteSpaceWithHyphens(val)); } setTitle(val); }; @@ -98,18 +93,19 @@ export const NewResourceModal = ({ isOpen, onClose }: NewResourceModalProps): Re * so that it closes the edit field, the id is set to the title. * * @param isOpened the value of the button when it is pressed - * @param isSave if the save button is pressed, keep id and title separate + * @param saveChanges if the save button is pressed, keep id and title separate */ - const handleClickEditButton = (isOpened: boolean, isSave: boolean) => { + const handleClickEditButton = (isOpened: boolean, saveChanges: boolean) => { setEditIdFieldOpen(isOpened); - - if (isSave) { + if (saveChanges) { setBothFieldsHaveSameValue(false); - } else { - if (!isOpened) { - setBothFieldsHaveSameValue(true); - // If we stop editing, set the ID to the title - if (title !== id) setId(title.replace(/\s/g, '-')); + return; + } + if (!isOpened) { + setBothFieldsHaveSameValue(true); + const shouldSetTitleToId = title !== id; + if (shouldSetTitleToId) { + setId(replaceWhiteSpaceWithHyphens(title)); } } }; @@ -139,7 +135,9 @@ export const NewResourceModal = ({ isOpen, onClose }: NewResourceModalProps): Re id={id} handleEditTitle={handleEditTitle} handleIdInput={handleIDInput} - handleClickEditButton={(isSave: boolean) => handleClickEditButton(!editIdFieldOpen, isSave)} + handleClickEditButton={(saveChanges: boolean) => + handleClickEditButton(!editIdFieldOpen, saveChanges) + } resourceIdExists={resourceIdExists} bothFieldsHaveSameValue={bothFieldsHaveSameValue} className={classes.resourceNameAndId} diff --git a/frontend/resourceadm/components/ResourceNameAndId/ResourceNameAndId.tsx b/frontend/resourceadm/components/ResourceNameAndId/ResourceNameAndId.tsx index 0310069df3f..27b6da64c41 100644 --- a/frontend/resourceadm/components/ResourceNameAndId/ResourceNameAndId.tsx +++ b/frontend/resourceadm/components/ResourceNameAndId/ResourceNameAndId.tsx @@ -5,51 +5,15 @@ import { MultiplyIcon, PencilWritingIcon, CheckmarkIcon } from '@navikt/aksel-ic import { useTranslation } from 'react-i18next'; export type ResourceNameAndIdProps = { - /** - * Flag to decide if the edit ID is open or not - */ isEditOpen: boolean; - /** - * The value of the title - */ title: string; - /** - * The text to display above the fields - */ text: string; - /** - * The value of the id - */ id: string; - /** - * Function to handle the editing of the title - * @param s the text written - * @returns void - */ handleEditTitle: (s: string) => void; - /** - * Function to handle the editing of the id - * @param s the text written - * @returns void - */ handleIdInput: (s: string) => void; - /** - * Function to be executed when edit button is clicked - * @param isSave flag for if it is to save or cancel - * @returns void - */ - handleClickEditButton: (isSave: boolean) => void; - /** - * Flag for id the ID already exists - */ + handleClickEditButton: (saveChanges: boolean) => void; resourceIdExists: boolean; - /** - * Flag for if ID and title has same display value - */ bothFieldsHaveSameValue: boolean; - /** - * Additional classes - */ className?: string; }; @@ -165,7 +129,7 @@ export const ResourceNameAndId = ({ return ( <> - {t('resourceadm.dashboard_resource_name_and_id_resource_id')} + {t('resourceadm.dashboard_resource_name_and_id_resource_id')}
diff --git a/frontend/resourceadm/hooks/mutations/index.ts b/frontend/resourceadm/hooks/mutations/index.ts index 36b0c6c969f..72a9a3021e7 100644 --- a/frontend/resourceadm/hooks/mutations/index.ts +++ b/frontend/resourceadm/hooks/mutations/index.ts @@ -2,3 +2,4 @@ export { useEditResourcePolicyMutation } from './useEditResourcePolicyMutation'; export { useCreateResourceMutation } from './useCreateResourceMutation'; export { useEditResourceMutation } from './useEditResourceMutation'; export { usePublishResourceMutation } from './usePublishResourceMutation'; +export { useImportResourceFromAltinn2Mutation } from './useImportResourceFromAltinn2Mutation'; diff --git a/frontend/resourceadm/hooks/mutations/useImportResourceFromAltinn2Mutation.ts b/frontend/resourceadm/hooks/mutations/useImportResourceFromAltinn2Mutation.ts new file mode 100644 index 00000000000..0eb10a2c267 --- /dev/null +++ b/frontend/resourceadm/hooks/mutations/useImportResourceFromAltinn2Mutation.ts @@ -0,0 +1,29 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useServicesContext } from 'app-shared/contexts/ServicesContext'; +import { QueryKey } from 'app-shared/types/QueryKey'; + +/** + * Mutation to import a resource from Altinn 2. + * + * @param org the organisation of the user + */ +export const useImportResourceFromAltinn2Mutation = (org: string) => { + const queryClient = useQueryClient(); + const { importResourceFromAltinn2 } = useServicesContext(); + + return useMutation({ + mutationFn: (payload: { environment: string; serviceCode: string; serviceEdition: string }) => + importResourceFromAltinn2( + org, + payload.environment, + payload.serviceCode, + payload.serviceEdition, + ), + onSuccess: (data) => { + queryClient.invalidateQueries({ + queryKey: [QueryKey.ImportAltinn2Resource, org], + }); + return data; + }, + }); +}; diff --git a/frontend/resourceadm/hooks/queries/index.ts b/frontend/resourceadm/hooks/queries/index.ts index cc95c1b4605..9415a08810d 100644 --- a/frontend/resourceadm/hooks/queries/index.ts +++ b/frontend/resourceadm/hooks/queries/index.ts @@ -5,3 +5,4 @@ export { useGetResourceListQuery } from './useGetResourceListQuery'; export { useSinlgeResourceQuery } from './useSinlgeResourceQuery'; export { useValidatePolicyQuery } from './useValidatePolicyQuery'; export { useValidateResourceQuery } from './useValidateResourceQuery'; +export { useGetAltinn2LinkServicesQuery } from './useGetAltinn2LinkServicesQuery'; diff --git a/frontend/resourceadm/hooks/queries/useGetAltinn2LinkServicesQuery.ts b/frontend/resourceadm/hooks/queries/useGetAltinn2LinkServicesQuery.ts new file mode 100644 index 00000000000..70f35fef23d --- /dev/null +++ b/frontend/resourceadm/hooks/queries/useGetAltinn2LinkServicesQuery.ts @@ -0,0 +1,25 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { useServicesContext } from 'app-shared/contexts/ServicesContext'; +import { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { AxiosError } from 'axios'; + +/** + * Query to get the list of services from Altinn 2. + * + * @param org the organisation of the user + * @param environment the environment to import from + * + * @returns UseQueryResult with a list of resources of Resource + */ +export const useGetAltinn2LinkServicesQuery = ( + org: string, + environment: string, +): UseQueryResult => { + const { getAltinn2LinkServices } = useServicesContext(); + + return useQuery( + [QueryKey.Altinn2Services, org, environment], + () => getAltinn2LinkServices(org, environment), + ); +}; diff --git a/frontend/resourceadm/types/global.ts b/frontend/resourceadm/types/global.ts index aea9edffd56..4705f334416 100644 --- a/frontend/resourceadm/types/global.ts +++ b/frontend/resourceadm/types/global.ts @@ -14,7 +14,3 @@ export interface DeployError { export type Translation = 'none' | 'title' | 'description' | 'rightDescription'; export type EnvironmentType = 'AT21' | 'AT22' | 'AT23' | 'AT24' | 'TT02' | 'PROD'; - -export interface ServiceType { - name: string; -} diff --git a/frontend/resourceadm/utils/mapperUtils/index.ts b/frontend/resourceadm/utils/mapperUtils/index.ts index bb686288cb1..81b31ad8d93 100644 --- a/frontend/resourceadm/utils/mapperUtils/index.ts +++ b/frontend/resourceadm/utils/mapperUtils/index.ts @@ -1 +1 @@ -export { sortResourceListByDateAndMap } from './mapperUtils'; +export { sortResourceListByDateAndMap, mapAltinn2LinkServiceToSelectOption } from './mapperUtils'; diff --git a/frontend/resourceadm/utils/mapperUtils/mapperUtils.test.ts b/frontend/resourceadm/utils/mapperUtils/mapperUtils.test.ts new file mode 100644 index 00000000000..7eef710bbf2 --- /dev/null +++ b/frontend/resourceadm/utils/mapperUtils/mapperUtils.test.ts @@ -0,0 +1,27 @@ +import { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService'; +import { mapAltinn2LinkServiceToSelectOption } from './mapperUtils'; + +describe('mapperUtils', () => { + describe('mapAltinn2LinkServiceToSelectOption', () => { + const mockLinkServices: Altinn2LinkService[] = [ + { + externalServiceCode: 'code1', + externalServiceEditionCode: 'edition1', + serviceName: 'name1', + }, + { + externalServiceCode: 'code2', + externalServiceEditionCode: 'edition2', + serviceName: 'name2', + }, + ]; + + it('should map Altinn2LinkService to SelectOption correctly', () => { + const result = mapAltinn2LinkServiceToSelectOption(mockLinkServices); + + expect(result).toHaveLength(mockLinkServices.length); + expect(result[0].value).toBe('code1-edition1-name1'); + expect(result[0].label).toBe('code1-edition1-name1'); + }); + }); +}); diff --git a/frontend/resourceadm/utils/mapperUtils/mapperUtils.ts b/frontend/resourceadm/utils/mapperUtils/mapperUtils.ts index 6318017c31a..ac4b14d9750 100644 --- a/frontend/resourceadm/utils/mapperUtils/mapperUtils.ts +++ b/frontend/resourceadm/utils/mapperUtils/mapperUtils.ts @@ -1,3 +1,4 @@ +import type { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService'; import type { ResourceListItem } from 'app-shared/types/ResourceAdm'; /** @@ -22,7 +23,7 @@ const formatDateFromBackendToDDMMYYYY = (dateString: string): string => { * @returns the sorted and mapped list */ export const sortResourceListByDateAndMap = ( - resourceList: ResourceListItem[] + resourceList: ResourceListItem[], ): ResourceListItem[] => { const sorted = resourceList.sort((a, b) => { return new Date(b.lastChanged).getTime() - new Date(a.lastChanged).getTime(); @@ -33,3 +34,18 @@ export const sortResourceListByDateAndMap = ( lastChanged: formatDateFromBackendToDDMMYYYY(r.lastChanged), })); }; + +/** + * Maps an Altinn2LinkService object to an object with value and label to be + * used for a Select option. + * + * @param linkServices the list of link services from Altinn 2 + * + * @returns an object that looks like this: { value: string, label: string } + */ +export const mapAltinn2LinkServiceToSelectOption = (linkServices: Altinn2LinkService[]) => { + return linkServices.map((ls: Altinn2LinkService) => ({ + value: `${ls.externalServiceCode}-${ls.externalServiceEditionCode}-${ls.serviceName}`, + label: `${ls.externalServiceCode}-${ls.externalServiceEditionCode}-${ls.serviceName}`, + })); +}; diff --git a/frontend/resourceadm/utils/stringUtils/index.ts b/frontend/resourceadm/utils/stringUtils/index.ts new file mode 100644 index 00000000000..484cd65408e --- /dev/null +++ b/frontend/resourceadm/utils/stringUtils/index.ts @@ -0,0 +1 @@ +export { replaceWhiteSpaceWithHyphens } from './stringUtils'; diff --git a/frontend/resourceadm/utils/stringUtils/stringUtils.test.ts b/frontend/resourceadm/utils/stringUtils/stringUtils.test.ts new file mode 100644 index 00000000000..7cf623f2fcc --- /dev/null +++ b/frontend/resourceadm/utils/stringUtils/stringUtils.test.ts @@ -0,0 +1,13 @@ +import { replaceWhiteSpaceWithHyphens } from './stringUtils'; + +describe('stringUtils', () => { + describe('replaceWhiteSpaceWithHyphens', () => { + const mockStringBefore: string = 'Test 123'; + const mockStringAfter: string = 'Test-123'; + + it('replaces white space with "-"', () => { + const result = replaceWhiteSpaceWithHyphens(mockStringBefore); + expect(result).toEqual(mockStringAfter); + }); + }); +}); diff --git a/frontend/resourceadm/utils/stringUtils/stringUtils.ts b/frontend/resourceadm/utils/stringUtils/stringUtils.ts new file mode 100644 index 00000000000..b9a3d6103df --- /dev/null +++ b/frontend/resourceadm/utils/stringUtils/stringUtils.ts @@ -0,0 +1,8 @@ +/** + * Replaces white space with hyphens in a string + * + * @param value the string to modify + * + * @returns the modified string where white space has been replaced with '-' + */ +export const replaceWhiteSpaceWithHyphens = (value: string): string => value.replace(/\s/g, '-');