Skip to content

Commit

Permalink
Merge branch 'main' into feat(ui-editor)/enable-component-poc-and-fee…
Browse files Browse the repository at this point in the history
…dback-form
  • Loading branch information
nkylstad authored Dec 2, 2024
2 parents c3df21f + ea8ca6c commit 8069ba4
Show file tree
Hide file tree
Showing 52 changed files with 1,029 additions and 613 deletions.
20 changes: 15 additions & 5 deletions backend/src/Designer/Controllers/ResourceAdminController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Altinn.Studio.Designer.ModelBinding.Constants;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Services.Interfaces;
using Altinn.Studio.Designer.Services.Models;
using Altinn.Studio.Designer.TypedHttpClients.ResourceRegistryOptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -34,18 +35,18 @@ public class ResourceAdminController : ControllerBase
private readonly CacheSettings _cacheSettings;
private readonly IOrgService _orgService;
private readonly IResourceRegistry _resourceRegistry;
private readonly ResourceRegistryIntegrationSettings _resourceRegistrySettings;
private readonly IEnvironmentsService _environmentsService;

public ResourceAdminController(IGitea gitea, IRepository repository, IResourceRegistryOptions resourceRegistryOptions, IMemoryCache memoryCache, IOptions<CacheSettings> cacheSettings, IOrgService orgService, IOptions<ResourceRegistryIntegrationSettings> resourceRegistryEnvironment, IResourceRegistry resourceRegistry)
public ResourceAdminController(IGitea gitea, IRepository repository, IResourceRegistryOptions resourceRegistryOptions, IMemoryCache memoryCache, IOptions<CacheSettings> cacheSettings, IOrgService orgService, IResourceRegistry resourceRegistry, IEnvironmentsService environmentsService)
{
_giteaApi = gitea;
_repository = repository;
_resourceRegistryOptions = resourceRegistryOptions;
_memoryCache = memoryCache;
_cacheSettings = cacheSettings.Value;
_orgService = orgService;
_resourceRegistrySettings = resourceRegistryEnvironment.Value;
_resourceRegistry = resourceRegistry;
_environmentsService = environmentsService;
}

[HttpPost]
Expand Down Expand Up @@ -175,12 +176,14 @@ public async Task<ActionResult<List<ListviewServiceResource>>> GetRepositoryReso

if (includeEnvResources)
{
foreach (string environment in _resourceRegistrySettings.Keys)
IEnumerable<string> environments = await GetEnvironmentsForOrg(org);
foreach (string environment in environments)
{
string cacheKey = $"resourcelist_${environment}";
if (!_memoryCache.TryGetValue(cacheKey, out List<ServiceResource> environmentResources))
{
environmentResources = await _resourceRegistry.GetResourceList(environment, false);

var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetPriority(CacheItemPriority.High)
.SetAbsoluteExpiration(new TimeSpan(0, _cacheSettings.DataNorgeApiCacheTimeout, 0));
Expand Down Expand Up @@ -239,7 +242,8 @@ public async Task<ActionResult<ServiceResourceStatus>> GetPublishStatusById(stri
PublishedVersions = []
};

foreach (string envir in _resourceRegistrySettings.Keys)
IEnumerable<string> environments = await GetEnvironmentsForOrg(org);
foreach (string envir in environments)
{
resourceStatus.PublishedVersions.Add(await AddEnvironmentResourceStatus(envir, id));
}
Expand Down Expand Up @@ -643,5 +647,11 @@ private string GetRepositoryName(string org)
{
return string.Format("{0}-resources", org);
}

private async Task<IEnumerable<string>> GetEnvironmentsForOrg(string org)
{
IEnumerable<EnvironmentModel> environments = await _environmentsService.GetOrganizationEnvironments(org);
return environments.Select(environment => environment.Name == "production" ? "prod" : environment.Name);
}
}
}
3 changes: 3 additions & 0 deletions backend/src/Designer/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
}
},
"ResourceRegistryIntegrationSettings": {
"YT01": {
"ResourceRegistryEnvBaseUrl": "https://platform.yt01.altinn.cloud"
},
"AT22": {
"ResourceRegistryEnvBaseUrl": "https://platform.at22.altinn.cloud",
"SblBridgeBaseUrl": "https://at22.altinn.cloud/sblbridge/"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { renderHookWithProviders } from '../test/mocks';
import {
queryParamKey,
openSettingsModalWithTabQueryKey,
useOpenSettingsModalBasedQueryParam,
} from './useOpenSettingsModalBasedQueryParam';
import { useSearchParams } from 'react-router-dom';
Expand Down Expand Up @@ -44,7 +44,7 @@ function setupSearchParamMock(searchParams: URLSearchParams): jest.Mock {

function buildSearchParams(queryParamValue: string): URLSearchParams {
const searchParams: URLSearchParams = new URLSearchParams();
searchParams.set(queryParamKey, queryParamValue);
searchParams.set(openSettingsModalWithTabQueryKey, queryParamValue);
return searchParams;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useSettingsModalContext } from '../contexts/SettingsModalContext';
import type { SettingsModalTabId } from '../types/SettingsModalTabId';
import { useSettingsModalMenuTabConfigs } from '../layout/PageHeader/SubHeader/SettingsModalButton/SettingsModal/hooks/useSettingsModalMenuTabConfigs';

export const queryParamKey: string = 'openSettingsModalWithTab';
export const openSettingsModalWithTabQueryKey: string = 'openSettingsModalWithTab';

export function useOpenSettingsModalBasedQueryParam(): void {
const [searchParams] = useSearchParams();
Expand All @@ -14,7 +14,9 @@ export function useOpenSettingsModalBasedQueryParam(): void {
const tabIds = settingsModalTabs.map(({ tabId }) => tabId);

useEffect((): void => {
const tabToOpen: SettingsModalTabId = searchParams.get(queryParamKey) as SettingsModalTabId;
const tabToOpen: SettingsModalTabId = searchParams.get(
openSettingsModalWithTabQueryKey,
) as SettingsModalTabId;
const shouldOpenModal: boolean = isValidTab(tabToOpen, tabIds);
if (shouldOpenModal) {
settingsRef.current.openSettings(tabToOpen);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AnsattportenLogin } from './AnsattportenLogin';
import { AnsattportenLogin, getRedirectUrl } from './AnsattportenLogin';
import { textMock } from '@studio/testing/mocks/i18nMock';

jest.mock('app-shared/api/paths');
Expand Down Expand Up @@ -40,10 +40,22 @@ describe('AnsattportenLogin', () => {
});
});

describe('getRedirectUrl', () => {
it('should build and return correct redirect url', () => {
mockWindowLocationHref();
const result = getRedirectUrl();
expect(result).toBe('/path/to/page?openSettingsModalWithTab=maskinporten');
});
});

function mockWindowLocationHref(): jest.Mock {
const hrefMock = jest.fn();
delete window.location;
window.location = { href: '' } as Location;
window.location = {
href: '',
origin: 'https://unit-test-com',
pathname: '/path/to/page',
} as Location;
Object.defineProperty(window.location, 'href', {
set: hrefMock,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { useTranslation } from 'react-i18next';
import { StudioButton, StudioParagraph } from '@studio/components';
import { EnterIcon } from '@studio/icons';
import { loginWithAnsattPorten } from 'app-shared/api/paths';
import { openSettingsModalWithTabQueryKey } from '../../../../../../../../../hooks/useOpenSettingsModalBasedQueryParam';
import type { SettingsModalTabId } from '../../../../../../../../../types/SettingsModalTabId';

export const AnsattportenLogin = (): ReactElement => {
const { t } = useTranslation();

const handleLoginWithAnsattporten = (): void => {
window.location.href = loginWithAnsattPorten(window.location.pathname + window.location.search);
window.location.href = loginWithAnsattPorten(getRedirectUrl());
};

return (
Expand Down Expand Up @@ -39,3 +41,10 @@ const LoginIcon = (): ReactElement => {
</div>
);
};

export function getRedirectUrl(): string {
const maskinportenTab: SettingsModalTabId = 'maskinporten';
const url = new URL(window.location.origin + window.location.pathname);
url.searchParams.set(openSettingsModalWithTabQueryKey, maskinportenTab);
return url.pathname + url.search;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.savingContainer {
display: flex;
align-items: center;
gap: var(--fds-spacing-1);
margin-top: var(--fds-spacing-4);
}

.savedIcon {
font-size: var(--fds-sizing-8);
color: var(--fds-semantic-text-neutral-subtle);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { SaveStatus } from './SaveStatus';
import { textMock } from '@studio/testing/mocks/i18nMock';

describe('SaveStatus', () => {
it('should display a spinner while pending', () => {
render(<SaveStatus isPending isSaved={false} />);

expect(
screen.getByText(textMock('settings_modal.maskinporten_tab_save_scopes_pending')),
).toBeInTheDocument();

expect(
screen.getByTitle(textMock('settings_modal.maskinporten_tab_save_scopes_pending_spinner')),
).toBeInTheDocument();
});

it('should render saved status with checkmark icon', () => {
render(<SaveStatus isPending={false} isSaved />);

expect(
screen.getByText(textMock('settings_modal.maskinporten_tab_save_scopes_complete')),
).toBeInTheDocument();
});

it('should render nothing when neither pending nor saved', () => {
const { container } = render(<SaveStatus isPending={false} isSaved={false} />);

expect(container).toBeEmptyDOMElement();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { type ReactElement } from 'react';
import classes from './SaveStatus.module.css';
import { StudioParagraph, StudioSpinner } from '@studio/components';
import { useTranslation } from 'react-i18next';
import { CheckmarkIcon } from '@studio/icons';

type SaveStatusProps = {
isPending: boolean;
isSaved: boolean;
};

export const SaveStatus = ({ isPending, isSaved }: SaveStatusProps): ReactElement => {
const { t } = useTranslation();

if (isPending) {
return (
<SaveStatusContent
text={t('settings_modal.maskinporten_tab_save_scopes_pending')}
isPending
/>
);
}
if (isSaved) {
return <SaveStatusContent text={t('settings_modal.maskinporten_tab_save_scopes_complete')} />;
}
return null;
};

type SaveStatusContentProps = {
text: string;
isPending?: boolean;
};

const SaveStatusContent = ({ text, isPending = false }: SaveStatusContentProps): ReactElement => {
const { t } = useTranslation();

return (
<div className={classes.savingContainer}>
<StudioParagraph size='sm'>{text}</StudioParagraph>
{isPending ? (
<StudioSpinner
spinnerTitle={t('settings_modal.maskinporten_tab_save_scopes_pending_spinner')}
size='sm'
/>
) : (
<CheckmarkIcon className={classes.savedIcon} />
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SaveStatus } from './SaveStatus';
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { type ChangeEvent, type ReactElement } from 'react';
import classes from './ScopeList.module.css';

import {
StudioCheckboxTable,
type StudioCheckboxTableRowElement,
Expand All @@ -25,6 +24,7 @@ import { GetInTouchWith } from 'app-shared/getInTouch';
import { EmailContactProvider } from 'app-shared/getInTouch/providers';
import { LoggedInTitle } from '../LoggedInTitle';
import { useUpdateSelectedMaskinportenScopesMutation } from 'app-development/hooks/mutations/useUpdateSelectedMaskinportenScopesMutation';
import { SaveStatus } from '../SaveStatus';

export type ScopeListProps = {
maskinPortenScopes: MaskinportenScope[];
Expand All @@ -33,8 +33,11 @@ export type ScopeListProps = {

export const ScopeList = ({ maskinPortenScopes, selectedScopes }: ScopeListProps): ReactElement => {
const { t } = useTranslation();
const { mutate: mutateSelectedMaskinportenScopes } =
useUpdateSelectedMaskinportenScopesMutation();
const {
mutate: mutateSelectedMaskinportenScopes,
isPending: isPendingSaveScopes,
isSuccess: scopesSaved,
} = useUpdateSelectedMaskinportenScopesMutation();

const checkboxTableRowElements: StudioCheckboxTableRowElement[] = mapScopesToRowElements(
maskinPortenScopes,
Expand Down Expand Up @@ -103,6 +106,7 @@ export const ScopeList = ({ maskinPortenScopes, selectedScopes }: ScopeListProps
))}
</StudioCheckboxTable.Body>
</StudioCheckboxTable>
<SaveStatus isPending={isPendingSaveScopes} isSaved={scopesSaved} />
</div>
);
};
Expand Down
8 changes: 7 additions & 1 deletion frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1023,9 +1023,12 @@
"settings_modal.maskinporten_tab_available_scopes_description_help": "Hvis du trenger tilgang til flere scopes i denne listen, <0> tar du kontakt med Altinn servicedesk.</0>",
"settings_modal.maskinporten_tab_available_scopes_description_help_link": "tar du kontakt med Altinn servicedesk.",
"settings_modal.maskinporten_tab_available_scopes_title": "Scopes for denne virksomheten",
"settings_modal.maskinporten_tab_description": "Maskinporten autentiserer og autoriserer API-er som skal brukes i apper. I Maskinporten kan du lage hvilke scopes en app skal ha fra og til andre systemer, for eksempel tilgang til persondata.",
"settings_modal.maskinporten_tab_description": "Maskinporten autentiserer og autoriserer API-er som skal brukes i apper. I Maskinporten kan du sette opp hvilke scopes en app skal ha fra og til andre systemer, for eksempel tilgang til persondata.",
"settings_modal.maskinporten_tab_login_with_ansattporten": "Logg inn med Ansattporten",
"settings_modal.maskinporten_tab_login_with_description": "Med Ansattporten logger du inn på vegne av virksomheten. Her kan du se og velge scopes.",
"settings_modal.maskinporten_tab_save_scopes_complete": "Lagret",
"settings_modal.maskinporten_tab_save_scopes_pending": "Lagrer...",
"settings_modal.maskinporten_tab_save_scopes_pending_spinner": "Lagrer scopes",
"settings_modal.maskinporten_tab_title": "Velg scopes fra Maskinporten",
"settings_modal.policy_tab_heading": "Tilganger",
"settings_modal.setup_tab_heading": "Oppsett",
Expand Down Expand Up @@ -1373,11 +1376,14 @@
"ux_editor.component_properties.stickyHeader": "Fest tittelraden",
"ux_editor.component_properties.style": "Stil",
"ux_editor.component_properties.subdomains": "Subdomener (kommaseparert)",
"ux_editor.component_properties.subform.choose_data_model": "Velg datamodell...",
"ux_editor.component_properties.subform.choose_layout_set": "Velg et underskjema...",
"ux_editor.component_properties.subform.choose_layout_set_description": "Velg først underskjemaet du vil bruke i Tabell for underskjema. Deretter kan du sette opp egenskapene for komponenten.",
"ux_editor.component_properties.subform.choose_layout_set_header": "Velg underskjemaet du vil bruke",
"ux_editor.component_properties.subform.choose_layout_set_label": "Velg et underskjema",
"ux_editor.component_properties.subform.create_layout_set_button": "Opprett et nytt underskjema",
"ux_editor.component_properties.subform.create_new_data_model": "Lag ny datamodell",
"ux_editor.component_properties.subform.create_new_data_model_label": "Navn på ny datamodell",
"ux_editor.component_properties.subform.created_layout_set_name": "Navn på underskjema",
"ux_editor.component_properties.subform.data_model_binding_label": "Velg datamodellknytning",
"ux_editor.component_properties.subform.data_model_empty_messsage": "Ingen tilgjengelige datamodeller",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ForwardedRef } from 'react';
import React from 'react';
import type { StudioTextResourceInputProps } from './StudioTextResourceInput';
import { StudioTextResourceInput } from './StudioTextResourceInput';
Expand All @@ -9,6 +10,9 @@ import { textResourcesMock } from '../../test-data/textResourcesMock';
import type { UserEvent } from '@testing-library/user-event';
import { userEvent } from '@testing-library/user-event';
import { getTextResourceById } from './utils';
import { testRefForwarding } from '../../test-utils/testRefForwarding';
import { testRootClassNameAppending } from '../../test-utils/testRootClassNameAppending';
import { testCustomAttributes } from '../../test-utils/testCustomAttributes';

// Test data:
const textResources: TextResource[] = textResourcesMock;
Expand Down Expand Up @@ -92,10 +96,28 @@ describe('StudioTextResourceInput', () => {
await switchToSearchMode(user);
expect(screen.getByText(currentId)).toBeInTheDocument();
});

it('Forwards the ref if given', () => {
testRefForwarding<HTMLInputElement>((ref) => renderTextResourceInput({}, ref), getValueField);
});

it('Appends the given class name to the root class', () => {
testRootClassNameAppending((className) => renderTextResourceInput({ className }));
});

it('Applies additional props to the input element', () => {
testCustomAttributes<HTMLInputElement, StudioTextResourceInputProps>(
renderTextResourceInput,
getValueField,
);
});
});

function renderTextResourceInput(props: Partial<StudioTextResourceInputProps> = {}): RenderResult {
return render(<StudioTextResourceInput {...defaultProps} {...props} />);
function renderTextResourceInput(
props: Partial<StudioTextResourceInputProps> = {},
ref?: ForwardedRef<HTMLInputElement>,
): RenderResult {
return render(<StudioTextResourceInput {...defaultProps} {...props} ref={ref} />);
}

function getValueField(): HTMLInputElement {
Expand Down
Loading

0 comments on commit 8069ba4

Please sign in to comment.