diff --git a/backend/src/Designer/wwwroot/designer/img/Altinn-studio.svg b/backend/src/Designer/wwwroot/designer/img/Altinn-studio.svg index 89ac6004ad3..59de4d4325b 100644 --- a/backend/src/Designer/wwwroot/designer/img/Altinn-studio.svg +++ b/backend/src/Designer/wwwroot/designer/img/Altinn-studio.svg @@ -1,4 +1,4 @@ - + diff --git a/frontend/app-development/config/routes.tsx b/frontend/app-development/config/routes.tsx index 8d1b23d825d..d00c124ab4b 100644 --- a/frontend/app-development/config/routes.tsx +++ b/frontend/app-development/config/routes.tsx @@ -47,7 +47,7 @@ export const routes: IRoute[] = [ activeSubHeaderSelection: TopBarMenu.About, activeLeftMenuSelection: 'Om appen', menu: 'about', - subapp: shouldDisplayFeature('newAdministration') ? Administration : LegacyAdministration, + subapp: shouldDisplayFeature('settingsModal') ? Administration : LegacyAdministration, }, { path: '/:org/:app/datamodel', diff --git a/frontend/app-development/features/administration/components/Administration.module.css b/frontend/app-development/features/administration/components/Administration.module.css index 6e30a37e373..cde2cbc0262 100644 --- a/frontend/app-development/features/administration/components/Administration.module.css +++ b/frontend/app-development/features/administration/components/Administration.module.css @@ -1,12 +1,12 @@ -.administration { +.pageContainer { background-color: #e6eff8; position: relative; } -.administration::before { +.pageContainer::before { background-image: url('/designer/img/Altinn-studio.svg'); - background-position: left -220px; - background-size: 1600px auto; + background-position: center 10px; + background-size: 100% auto; background-repeat: no-repeat; content: ' '; @@ -36,7 +36,7 @@ .content { background-color: white; - border-radius: 6px; + border-radius: 4px; box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; @@ -77,6 +77,14 @@ padding: var(--fds-spacing-4); } +.asideBlock { + background-color: #fbfbfc; + border: solid 2px #efefef; + border-radius: 4px; + + padding: var(--fds-spacing-4); +} + .divider { background-color: #efefef; border: none; @@ -86,14 +94,6 @@ width: 100%; } -/* TODO: remove when all sections are implemented */ -.placeholder { - background-color: #fbfbfc; - border: solid 2px #efefef; - border-radius: 6px; - height: 500px; -} - @media (min-width: 960px) { .content { flex-direction: row; @@ -103,9 +103,3 @@ display: block; } } - -@media (min-width: 1600px) { - .administration::before { - background-position: center -220px; - } -} diff --git a/frontend/app-development/features/administration/components/Administration.test.tsx b/frontend/app-development/features/administration/components/Administration.test.tsx index de1cb4c6a68..199611bf179 100644 --- a/frontend/app-development/features/administration/components/Administration.test.tsx +++ b/frontend/app-development/features/administration/components/Administration.test.tsx @@ -16,6 +16,8 @@ describe('Administration', () => { startUrl: `${APP_DEVELOPMENT_BASENAME}/${org}/${app}`, queries: { ...queriesMock, + getEnvironments: jest.fn().mockImplementation(() => Promise.resolve([])), + getOrgList: jest.fn().mockImplementation(() => Promise.resolve({ orgs: [] })), getAppConfig: jest.fn().mockImplementation(() => Promise.resolve({ serviceName: title, diff --git a/frontend/app-development/features/administration/components/Administration.tsx b/frontend/app-development/features/administration/components/Administration.tsx index 666c2d7329d..f21bfff38b8 100644 --- a/frontend/app-development/features/administration/components/Administration.tsx +++ b/frontend/app-development/features/administration/components/Administration.tsx @@ -6,6 +6,8 @@ import { Heading } from '@digdir/design-system-react'; import { toast } from 'react-toastify'; import { useTranslation } from 'react-i18next'; import { Documentation } from './Documentation'; +import { AppEnvironments } from './AppEnvironments'; +import { AppLogs } from './AppLogs'; import { Navigation } from './Navigation'; export const Administration = () => { @@ -18,24 +20,31 @@ export const Administration = () => { } return ( -
+
{appConfigData?.serviceName || app}
-
- {/* APP STATUS PLACEHOLDER */} +
+ +
+
+
diff --git a/frontend/app-development/features/administration/components/AppEnvironments.module.css b/frontend/app-development/features/administration/components/AppEnvironments.module.css new file mode 100644 index 00000000000..1cf7c57a28f --- /dev/null +++ b/frontend/app-development/features/administration/components/AppEnvironments.module.css @@ -0,0 +1,5 @@ +.appEnvironments { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: var(--fds-spacing-4); +} diff --git a/frontend/app-development/features/administration/components/AppEnvironments.test.tsx b/frontend/app-development/features/administration/components/AppEnvironments.test.tsx new file mode 100644 index 00000000000..061a5808835 --- /dev/null +++ b/frontend/app-development/features/administration/components/AppEnvironments.test.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { screen, waitForElementToBeRemoved } from '@testing-library/react'; +import { AppEnvironments } from './AppEnvironments'; +import { APP_DEVELOPMENT_BASENAME } from 'app-shared/constants'; +import { renderWithProviders } from '../../../test/testUtils'; +import { queriesMock } from 'app-development/test/mocks'; +import { textMock } from '../../../../testing/mocks/i18nMock'; + +// Test data +const org = 'org'; +const app = 'app'; + +const render = (queries = {}) => { + return renderWithProviders(, { + startUrl: `${APP_DEVELOPMENT_BASENAME}/${org}/${app}`, + queries: { + ...queriesMock, + ...queries, + }, + }); +}; + +describe('AppEnvironments', () => { + it('shows loading spinner when loading required data', () => { + render({ + getEnvironments: jest.fn().mockImplementation(() => Promise.resolve([])), + getOrgList: jest.fn().mockImplementation(() => Promise.resolve([])), + }); + + expect(screen.getByText(textMock('general.loading'))).toBeInTheDocument(); + }); + + it('shows error message if an error occured while fetching required data', async () => { + render({ + getEnvironments: jest.fn().mockImplementation(() => Promise.reject()), + getOrgList: jest.fn().mockImplementation(() => Promise.reject()), + }); + + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); + + expect(screen.getByText(textMock('administration.app_environments_error'))).toBeInTheDocument(); + }); + + it('shows no environments message when organization has no environment', async () => { + render({ + getEnvironments: jest.fn().mockImplementation(() => Promise.resolve([])), + getOrgList: jest.fn().mockImplementation(() => Promise.resolve({ orgs: [] })), + }); + + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); + + expect( + screen.getByRole('heading', { name: textMock('app_publish.no_env_title') }), + ).toBeInTheDocument(); + expect(screen.getByText(textMock('app_publish.no_env_1'))).toBeInTheDocument(); + expect(screen.getByText(textMock('app_publish.no_env_2'))).toBeInTheDocument(); + }); + + it('shows statuses when organization has environments', async () => { + const envName = 'tt02'; + const envType = 'test'; + render({ + getDeployments: jest.fn().mockImplementation(() => + Promise.resolve({ + results: [ + { + tagName: '1', + envName, + deployedInEnv: false, + build: { + id: '14381045', + status: 'completed', + result: 'succeeded', + started: '2023-10-03T09:57:31.238Z', + finished: '2023-10-03T09:57:41.29Z', + }, + created: '2023-10-03T11:57:31.072013+02:00', + createdBy: 'test', + app, + org, + }, + ], + }), + ), + + getEnvironments: jest.fn().mockImplementation(() => + Promise.resolve([ + { + appsUrl: 'http://host.docker.internal:6161', + platformUrl: 'http://host.docker.internal:6161', + hostname: 'host.docker.internal:6161', + appPrefix: 'apps', + platformPrefix: 'platform', + name: envName, + type: envType, + }, + ]), + ), + getOrgList: jest + .fn() + .mockImplementation(() => + Promise.resolve({ orgs: { [org]: { environments: [envName] } } }), + ), + }); + + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); + + expect(screen.getByRole('heading', { name: envName })).toBeInTheDocument(); + expect(screen.getByText(textMock('administration.unavailable'))).toBeInTheDocument(); + expect(screen.getByText(textMock('administration.go_to_build_log'))).toBeInTheDocument(); + }); +}); diff --git a/frontend/app-development/features/administration/components/AppEnvironments.tsx b/frontend/app-development/features/administration/components/AppEnvironments.tsx new file mode 100644 index 00000000000..cb8487c6345 --- /dev/null +++ b/frontend/app-development/features/administration/components/AppEnvironments.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams'; +import { useEnvironmentsQuery, useOrgListQuery } from 'app-development/hooks/queries'; +import { AltinnSpinner } from 'app-shared/components'; +import { ICreateAppDeploymentEnvObject } from 'app-development/sharedResources/appDeployment/types'; +import { DeployEnvironment } from 'app-shared/types/DeployEnvironment'; +import { AppStatus } from './AppStatus'; +import { Alert } from '@digdir/design-system-react'; +import { NoEnvironmentsAlert } from './NoEnvironmentsAlert'; +import classes from './AppEnvironments.module.css'; + +export const AppEnvironments = () => { + const { org } = useStudioUrlParams(); + const { t } = useTranslation(); + + const { + data: environmentList = [], + isLoading: envIsLoading, + isError: envIsError, + } = useEnvironmentsQuery({ hideDefaultError: true }); + const { + data: orgs = { orgs: {} }, + isLoading: orgsIsLoading, + isError: orgsIsError, + } = useOrgListQuery({ hideDefaultError: true }); + + if (envIsLoading || orgsIsLoading) return ; + + if (envIsError || orgsIsError) + return {t('administration.app_environments_error')}; + + const selectedOrg = orgs.orgs[org]; + const hasNoEnvironments = !(selectedOrg?.environments?.length ?? 0); + + if (hasNoEnvironments) { + return ; + } + + const orgEnvironments: ICreateAppDeploymentEnvObject[] = environmentList.filter( + (env: DeployEnvironment) => selectedOrg.environments.includes(env.name), + ); + + return ( +
+ {orgEnvironments.map((orgEnvironment: DeployEnvironment) => { + return ( + + ); + })} +
+ ); +}; diff --git a/frontend/app-development/features/administration/components/AppLogs.module.css b/frontend/app-development/features/administration/components/AppLogs.module.css new file mode 100644 index 00000000000..212ee4c37ac --- /dev/null +++ b/frontend/app-development/features/administration/components/AppLogs.module.css @@ -0,0 +1,30 @@ +.appLogs { + display: flex; + flex-direction: column; + gap: var(--fds-spacing-4); + justify-content: space-between; + + max-height: 275px; +} + +.appLogsTitle { + font-weight: unset !important; +} + +.logs { + background-color: white; + border-radius: 4px; + display: flex; + flex-direction: column; + gap: var(--fds-spacing-6); + margin: 0; + padding: var(--fds-spacing-4); + padding-left: var(--fds-spacing-8); + + overflow: auto; +} + +.logTitle { + font-weight: bold; + margin-bottom: var(--fds-spacing-2); +} diff --git a/frontend/app-development/features/administration/components/AppLogs.test.tsx b/frontend/app-development/features/administration/components/AppLogs.test.tsx new file mode 100644 index 00000000000..003324f8308 --- /dev/null +++ b/frontend/app-development/features/administration/components/AppLogs.test.tsx @@ -0,0 +1,216 @@ +import React from 'react'; +import { screen, waitForElementToBeRemoved } from '@testing-library/react'; +import { AppLogs } from './AppLogs'; +import { APP_DEVELOPMENT_BASENAME } from 'app-shared/constants'; +import { renderWithProviders } from '../../../test/testUtils'; +import { queriesMock } from 'app-development/test/mocks'; +import { textMock } from '../../../../testing/mocks/i18nMock'; + +// Test data +const org = 'ttd'; +const app = 'test-ttd'; + +const render = (queries = {}) => { + return renderWithProviders(, { + startUrl: `${APP_DEVELOPMENT_BASENAME}/${org}/${app}`, + queries: { + ...queriesMock, + ...queries, + }, + }); +}; + +describe('AppLogs', () => { + it('shows loading spinner when loading required data', () => { + render({ + getEnvironments: jest.fn().mockImplementation(() => Promise.resolve([])), + }); + + expect(screen.getByText(textMock('general.loading'))).toBeInTheDocument(); + }); + + it('shows error message if an error occured while fetching required data', async () => { + render({ + getEnvironments: jest.fn().mockImplementation(() => Promise.reject()), + }); + + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); + + expect(screen.getByText(textMock('administration.app_logs_error'))).toBeInTheDocument(); + }); + + it('shows list of deployments', async () => { + render({ + getDeployments: jest.fn().mockImplementation(() => + Promise.resolve({ + results: [ + { + tagName: '2', + envName: 'production', + deployedInEnv: true, + build: { + id: '14381045', + status: 'completed', + result: 'succeeded', + started: '2023-10-03T09:57:31.238Z', + finished: '2023-10-03T09:57:41.29Z', + }, + created: '2023-10-03T11:57:31.072013+02:00', + createdBy: 'test', + app, + org, + }, + { + tagName: '1', + envName: 'tt02', + deployedInEnv: true, + build: { + id: '14381045', + status: 'completed', + result: 'succeeded', + started: '2023-10-03T09:57:31.238Z', + finished: '2023-10-03T09:57:41.29Z', + }, + created: '2023-10-03T11:57:31.072013+02:00', + createdBy: 'test', + app, + org, + }, + ], + }), + ), + getEnvironments: jest.fn().mockImplementation(() => + Promise.resolve([ + { + appsUrl: 'http://host.docker.internal:6161', + platformUrl: 'http://host.docker.internal:6161', + hostname: 'host.docker.internal:6161', + appPrefix: 'apps', + platformPrefix: 'platform', + name: 'production', + type: 'production', + }, + { + appsUrl: 'http://host.docker.internal:6161', + platformUrl: 'http://host.docker.internal:6161', + hostname: 'host.docker.internal:6161', + appPrefix: 'apps', + platformPrefix: 'platform', + name: 'tt02', + type: 'test', + }, + { + appsUrl: 'http://host.docker.internal:6161', + platformUrl: 'http://host.docker.internal:6161', + hostname: 'host.docker.internal:6161', + appPrefix: 'apps', + platformPrefix: 'platform', + name: 'at21', + type: 'test', + }, + { + appsUrl: 'http://host.docker.internal:6161', + platformUrl: 'http://host.docker.internal:6161', + hostname: 'host.docker.internal:6161', + appPrefix: 'apps', + platformPrefix: 'platform', + name: 'at22', + type: 'test', + }, + ]), + ), + }); + + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); + + expect( + screen.getByRole('heading', { name: textMock('administration.activity') }), + ).toBeInTheDocument(); + expect( + screen.getByText( + textMock('administration.app_logs_title', { + tagName: '2', + environment: textMock('general.production_environment'), + envName: 'PRODUCTION', + }), + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + textMock('administration.app_logs_title', { tagName: '1', envName: 'TT02' }), + ), + ).toBeInTheDocument(); + }); + + it('shows no activity message when deployments are empty', async () => { + render({ + getDeployments: jest.fn().mockImplementation(() => + Promise.resolve({ + results: [ + { + tagName: '2', + envName: 'production', + deployedInEnv: true, + build: { + id: '14381045', + status: 'completed', + result: '', + started: '2023-10-03T09:57:31.238Z', + finished: '2023-10-03T09:57:41.29Z', + }, + created: '2023-10-03T11:57:31.072013+02:00', + createdBy: 'test', + app, + org, + }, + { + tagName: '1', + envName: 'tt02', + deployedInEnv: true, + build: { + id: '14381045', + status: 'completed', + result: 'succeeded', + started: '2023-10-03T09:57:31.238Z', + finished: null, + }, + created: '2023-10-03T11:57:31.072013+02:00', + createdBy: 'test', + app, + org, + }, + ], + }), + ), + getEnvironments: jest.fn().mockImplementation(() => + Promise.resolve([ + { + appsUrl: 'http://host.docker.internal:6161', + platformUrl: 'http://host.docker.internal:6161', + hostname: 'host.docker.internal:6161', + appPrefix: 'apps', + platformPrefix: 'platform', + name: 'production', + type: 'production', + }, + { + appsUrl: 'http://host.docker.internal:6161', + platformUrl: 'http://host.docker.internal:6161', + hostname: 'host.docker.internal:6161', + appPrefix: 'apps', + platformPrefix: 'platform', + name: 'tt02', + type: 'test', + }, + ]), + ), + }); + + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); + + expect( + screen.getByRole('heading', { name: textMock('administration.activity') }), + ).toBeInTheDocument(); + expect(screen.getByText(textMock('administration.no_activity'))).toBeInTheDocument(); + }); +}); diff --git a/frontend/app-development/features/administration/components/AppLogs.tsx b/frontend/app-development/features/administration/components/AppLogs.tsx new file mode 100644 index 00000000000..bf01603fd97 --- /dev/null +++ b/frontend/app-development/features/administration/components/AppLogs.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import classes from './AppLogs.module.css'; +import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams'; +import { useAppDeploymentsQuery, useEnvironmentsQuery } from 'app-development/hooks/queries'; +import { useTranslation } from 'react-i18next'; +import { AltinnSpinner } from 'app-shared/components'; +import { DeploymentStatus } from 'app-development/features/appPublish/components/appDeploymentComponent'; +import { DeployEnvironment } from 'app-shared/types/DeployEnvironment'; +import { IDeployment } from 'app-development/sharedResources/appDeployment/types'; +import { Alert, Heading } from '@digdir/design-system-react'; +import { formatDateDDMMYY, formatTimeHHmm } from 'app-shared/pure/date-format'; + +export const AppLogs = () => { + const { org, app } = useStudioUrlParams(); + const { t } = useTranslation(); + + const { + data: appDeployments = [], + isLoading: isLoadingDeploys, + isError: deploysHasError, + } = useAppDeploymentsQuery(org, app, { hideDefaultError: true }); + const { + data: environmentList = [], + isLoading: envIsLoading, + isError: envIsError, + } = useEnvironmentsQuery({ hideDefaultError: true }); + + if (isLoadingDeploys || envIsLoading) return ; + + if (deploysHasError || envIsError) + return {t('administration.app_logs_error')}; + + const succeededDeployments = appDeployments.filter( + (deployment: IDeployment) => + deployment.build.result === DeploymentStatus.succeeded && deployment.build.finished !== null, + ); + const hasSucceededDeployments = succeededDeployments.length > 0; + + const formatDateTime = (dateAsString: string): string => { + return t('general.date_time_format', { + date: formatDateDDMMYY(dateAsString), + time: formatTimeHHmm(dateAsString), + }); + }; + + const keyToTranslationMap: Record = { + production: t('general.production_environment'), + }; + + return ( +
+ + {t('administration.activity')} + +
    + {hasSucceededDeployments ? ( + succeededDeployments.map((appDeployment) => { + const environmentType = environmentList + .find((env: DeployEnvironment) => env.name === appDeployment.envName) + ?.type.toLowerCase(); + return ( +
  • +
    + {t('administration.app_logs_title', { + tagName: appDeployment.tagName, + environment: keyToTranslationMap[environmentType], + envName: appDeployment.envName?.toUpperCase() || '', + })} +
    +
    + {t('administration.app_logs_created', { + createdBy: appDeployment.createdBy, + createdDateTime: formatDateTime(appDeployment.created), + })} +
    +
  • + ); + }) + ) : ( +
  • {t('administration.no_activity')}
  • + )} +
+
+ ); +}; diff --git a/frontend/app-development/features/administration/components/AppStatus.module.css b/frontend/app-development/features/administration/components/AppStatus.module.css new file mode 100644 index 00000000000..9e36d0852e6 --- /dev/null +++ b/frontend/app-development/features/administration/components/AppStatus.module.css @@ -0,0 +1,17 @@ +.alert { + border-radius: 4px; + box-shadow: unset; + display: block; + position: relative; +} + +/* SVG styling is to move the alert-icon from left to right */ +.alert svg { + position: absolute; + right: var(--fds-spacing-3); + top: var(--fds-spacing-3); +} + +.heading { + text-transform: uppercase; +} diff --git a/frontend/app-development/features/administration/components/AppStatus.test.tsx b/frontend/app-development/features/administration/components/AppStatus.test.tsx new file mode 100644 index 00000000000..ee3cea1a384 --- /dev/null +++ b/frontend/app-development/features/administration/components/AppStatus.test.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { screen, waitForElementToBeRemoved } from '@testing-library/react'; +import { AppStatus } from './AppStatus'; +import { APP_DEVELOPMENT_BASENAME } from 'app-shared/constants'; +import { renderWithProviders } from '../../../test/testUtils'; +import { queriesMock } from 'app-development/test/mocks'; +import { textMock } from '../../../../testing/mocks/i18nMock'; + +// Test data +const org = 'ttd'; +const app = 'test-ttd'; +const envNameTest = 'tt02'; +const envTypeTest = 'test'; + +const render = (queries = {}, envName = envNameTest, envType = envTypeTest) => { + return renderWithProviders(, { + startUrl: `${APP_DEVELOPMENT_BASENAME}/${org}/${app}`, + queries: { + ...queriesMock, + ...queries, + }, + }); +}; + +describe('AppStatus', () => { + it('shows loading spinner when loading required data', () => { + render(); + + expect(screen.getByText(textMock('general.loading'))).toBeInTheDocument(); + }); + + it('shows error message if an error occured while fetching required data', async () => { + render({ + getDeployments: jest.fn().mockImplementation(() => Promise.reject()), + }); + + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); + + expect(screen.getByText(textMock('administration.app_status_error'))).toBeInTheDocument(); + }); + + it('shows production when environment is production', async () => { + const envNameProduction = 'production'; + const envTypeProduction = 'production'; + render( + { + getDeployments: jest.fn().mockImplementation(() => + Promise.resolve({ + results: [ + { + tagName: '1', + envName: envNameProduction, + deployedInEnv: true, + build: { + id: '14381045', + status: 'completed', + result: 'succeeded', + started: '2023-10-03T09:57:31.238Z', + finished: '2023-10-03T09:57:41.29Z', + }, + created: '2023-10-03T11:57:31.072013+02:00', + createdBy: 'test', + app, + org, + }, + ], + }), + ), + }, + envNameProduction, + envTypeProduction, + ); + + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); + + expect( + screen.getByRole('heading', { name: textMock('general.production') }), + ).toBeInTheDocument(); + }); + + it('shows success alert when application deployed', async () => { + render({ + getDeployments: jest.fn().mockImplementation(() => + Promise.resolve({ + results: [ + { + tagName: '1', + envName: envNameTest, + deployedInEnv: true, + build: { + id: '14381045', + status: 'completed', + result: 'succeeded', + started: '2023-10-03T09:57:31.238Z', + finished: '2023-10-03T09:57:41.29Z', + }, + created: '2023-10-03T11:57:31.072013+02:00', + createdBy: 'test', + app, + org, + }, + ], + }), + ), + }); + + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); + + expect(screen.getByRole('heading', { name: envNameTest })).toBeInTheDocument(); + expect(screen.getByText(textMock('administration.success'))).toBeInTheDocument(); + expect(screen.getByText(textMock('administration.last_published'))).toBeInTheDocument(); + }); + + it('shows no app alert when application not deployed', async () => { + render({ + getDeployments: jest.fn().mockImplementation(() => + Promise.resolve({ + results: [], + }), + ), + }); + + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); + + expect(screen.getByRole('heading', { name: envNameTest })).toBeInTheDocument(); + expect(screen.getByText(textMock('administration.no_app'))).toBeInTheDocument(); + expect(screen.getByText(textMock('administration.go_to_publish'))).toBeInTheDocument(); + }); + + it('shows unavailable alert when application not reachable', async () => { + render({ + getDeployments: jest.fn().mockImplementation(() => + Promise.resolve({ + results: [ + { + tagName: '1', + envName: envNameTest, + deployedInEnv: false, + build: { + id: '14381045', + status: 'completed', + result: 'succeeded', + started: '2023-10-03T09:57:31.238Z', + finished: '2023-10-03T09:57:41.29Z', + }, + created: '2023-10-03T11:57:31.072013+02:00', + createdBy: 'test', + app, + org, + }, + ], + }), + ), + }); + + await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading'))); + + expect(screen.getByRole('heading', { name: envNameTest })).toBeInTheDocument(); + expect(screen.getByText(textMock('administration.unavailable'))).toBeInTheDocument(); + expect(screen.getByText(textMock('administration.go_to_build_log'))).toBeInTheDocument(); + }); +}); diff --git a/frontend/app-development/features/administration/components/AppStatus.tsx b/frontend/app-development/features/administration/components/AppStatus.tsx new file mode 100644 index 00000000000..03f7aea40a8 --- /dev/null +++ b/frontend/app-development/features/administration/components/AppStatus.tsx @@ -0,0 +1,161 @@ +import React, { useMemo } from 'react'; +import classes from './AppStatus.module.css'; +import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams'; +import { useAppDeploymentsQuery } from 'app-development/hooks/queries'; +import { Trans, useTranslation } from 'react-i18next'; +import { Alert, Heading, Paragraph } from '@digdir/design-system-react'; +import { AltinnSpinner } from 'app-shared/components'; +import { DeploymentStatus } from 'app-development/features/appPublish/components/appDeploymentComponent'; +import { formatDateDDMMYY, formatTimeHHmm } from 'app-shared/pure/date-format'; +import { IDeployment } from 'app-development/sharedResources/appDeployment/types'; +import { getReleaseBuildPipelineLink } from 'app-development/utils/urlHelper'; +import { publishPath } from 'app-shared/api/paths'; + +export type AppStatusProps = { + envName: string; + envType: string; +}; + +export const AppStatus = ({ envName, envType }: AppStatusProps) => { + const { org, app } = useStudioUrlParams(); + const { t } = useTranslation(); + + const { + data: appDeployments = [], + isLoading: deploysAreLoading, + isError: deploysAreError, + } = useAppDeploymentsQuery(org, app, { hideDefaultError: true }); + + const deployHistory: IDeployment[] = appDeployments.filter((x) => x.envName === envName); + + const latestDeploy = deployHistory ? deployHistory[0] : null; + const deploymentInEnv = deployHistory.find((d) => d.deployedInEnv); + + const { deployInProgress, deploymentStatus } = useMemo(() => { + if (!latestDeploy) { + return { deployInProgress: false, deploymentStatus: null }; + } + + if (latestDeploy.build.finished === null) { + return { deployInProgress: true, deploymentStatus: DeploymentStatus.inProgress }; + } + + if (latestDeploy.build.finished && latestDeploy.build.result) { + return { deployInProgress: false, deploymentStatus: latestDeploy.build.result }; + } + + return { deployInProgress: false, deploymentStatus: null }; + }, [latestDeploy]); + + const appDeployedAndReachable = !!deploymentInEnv; + + const deployFailed = latestDeploy && deploymentStatus === DeploymentStatus.failed; + + const deployedVersionNotReachable = + latestDeploy && !appDeployedAndReachable && deploymentStatus === DeploymentStatus.succeeded; + + const noAppDeployed = !latestDeploy || deployInProgress; + + const formatDateTime = (dateAsString: string): string => { + return t('general.date_time_format', { + date: formatDateDDMMYY(dateAsString), + time: formatTimeHHmm(dateAsString), + }); + }; + + if (deploysAreLoading) return ; + + if (deploysAreError) + return ( + + + + ); + + if (appDeployedAndReachable && !deployInProgress) { + return ( + + } + > + ); + } + + if (noAppDeployed || (deployFailed && !appDeployedAndReachable)) { + return ( + + + + } + /> + ); + } + + if (deployedVersionNotReachable) { + return ( + + + + } + /> + ); + } +}; + +type DeploymentStatusInfoProps = { + envType: string; + envName: string; + severity: 'success' | 'warning' | 'info'; + content: string; + footer: string | JSX.Element; +}; +const DeploymentStatusInfo = ({ + envType, + envName, + severity, + content, + footer, +}: DeploymentStatusInfoProps) => { + const { t } = useTranslation(); + const isProduction = envType.toLowerCase() === 'production'; + const headingText = isProduction ? t('general.production') : envName; + + return ( + + + {headingText} + + + {content} + + {footer} + + ); +}; diff --git a/frontend/app-development/features/administration/components/Documentation.module.css b/frontend/app-development/features/administration/components/Documentation.module.css index a1b1dd155b8..35e229d2afa 100644 --- a/frontend/app-development/features/administration/components/Documentation.module.css +++ b/frontend/app-development/features/administration/components/Documentation.module.css @@ -1,12 +1,7 @@ .documentation { - background-color: #fbfbfc; - border: solid 2px #efefef; - border-radius: 6px; - font-family: var(--studio-font-family); font-size: 15px; - padding: var(--fds-spacing-4); position: relative; } diff --git a/frontend/app-development/features/administration/components/NoEnvironmentsAlert/NoEnviormentsAlert.test.tsx b/frontend/app-development/features/administration/components/NoEnvironmentsAlert/NoEnviormentsAlert.test.tsx new file mode 100644 index 00000000000..595336c4efa --- /dev/null +++ b/frontend/app-development/features/administration/components/NoEnvironmentsAlert/NoEnviormentsAlert.test.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { NoEnvironmentsAlert } from './NoEnvironmentsAlert'; +import { textMock } from '../../../../../testing/mocks/i18nMock'; + +it('should render no environments information', () => { + render(); + expect( + screen.getByRole('heading', { name: textMock('app_publish.no_env_title'), level: 2 }), + ).toBeInTheDocument(); + + expect(screen.getByText(textMock('app_publish.no_env_1'))); + expect(screen.getByText(textMock('app_publish.no_env_2'))); +}); diff --git a/frontend/app-development/features/administration/components/NoEnvironmentsAlert/NoEnvironmentsAlert.tsx b/frontend/app-development/features/administration/components/NoEnvironmentsAlert/NoEnvironmentsAlert.tsx new file mode 100644 index 00000000000..936f674784d --- /dev/null +++ b/frontend/app-development/features/administration/components/NoEnvironmentsAlert/NoEnvironmentsAlert.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import cn from 'classnames'; +import { Alert, AlertProps, Heading, Paragraph } from '@digdir/design-system-react'; + +type NoEnvironmentsAlertProps = AlertProps; +export const NoEnvironmentsAlert = ({ ...rest }: NoEnvironmentsAlertProps) => { + const { t } = useTranslation(); + + return ( + + + {t('app_publish.no_env_title')} + + + + + + + + + + + + + ); +}; diff --git a/frontend/app-development/features/administration/components/NoEnvironmentsAlert/index.ts b/frontend/app-development/features/administration/components/NoEnvironmentsAlert/index.ts new file mode 100644 index 00000000000..8454335315e --- /dev/null +++ b/frontend/app-development/features/administration/components/NoEnvironmentsAlert/index.ts @@ -0,0 +1 @@ +export * from './NoEnvironmentsAlert'; diff --git a/frontend/app-development/features/appPublish/components/appDeploymentComponent.tsx b/frontend/app-development/features/appPublish/components/appDeploymentComponent.tsx index 40bb47d2a31..d576b3d2bba 100644 --- a/frontend/app-development/features/appPublish/components/appDeploymentComponent.tsx +++ b/frontend/app-development/features/appPublish/components/appDeploymentComponent.tsx @@ -2,7 +2,14 @@ import React, { useEffect, useMemo, useState } from 'react'; import classes from './appDeploymentComponent.module.css'; import { AltinnLink, AltinnSpinner } from 'app-shared/components'; import { DeployDropdown } from './deploy/DeployDropdown'; -import { Table, TableRow, TableHeader, TableCell, TableBody, Link } from '@digdir/design-system-react'; +import { + Table, + TableRow, + TableHeader, + TableCell, + TableBody, + Link, +} from '@digdir/design-system-react'; import { formatDateTime } from 'app-shared/pure/date-format'; import { useCreateDeploymentMutation } from '../../../hooks/mutations'; import { useTranslation, Trans } from 'react-i18next'; @@ -67,9 +74,9 @@ export const AppDeploymentComponent = ({ deployHistory.filter( (deployment: IDeployment) => deployment.build.result === DeploymentStatus.succeeded && - deployment.build.finished !== null + deployment.build.finished !== null, ), - [deployHistory] + [deployHistory], ); const latestDeploy = deployHistory ? deployHistory[0] : null; const deploymentInEnv = deployHistory ? deployHistory.find((d) => d.deployedInEnv) : false; @@ -93,7 +100,9 @@ export const AppDeploymentComponent = ({ if (deployPermission && latestDeploy && deployedVersionNotReachable) { toast.error(() => ( - tjenesteeier@altinn.no + + tjenesteeier@altinn.no + )); } @@ -103,7 +112,9 @@ export const AppDeploymentComponent = ({ if (deployPermission && (deployFailed || mutation.isError)) { toast.error(() => ( - tjenesteeier@altinn.no + + tjenesteeier@altinn.no + )); } @@ -137,7 +148,7 @@ export const AppDeploymentComponent = ({ {!deployPermission && (
- +
{t('app_publish.missing_rights', { envName, orgName })}
diff --git a/frontend/app-development/features/appPublish/containers/releaseContainer.tsx b/frontend/app-development/features/appPublish/containers/releaseContainer.tsx index e2b540d4185..805417a9097 100644 --- a/frontend/app-development/features/appPublish/containers/releaseContainer.tsx +++ b/frontend/app-development/features/appPublish/containers/releaseContainer.tsx @@ -29,7 +29,7 @@ export function ReleaseContainer() { const { data: masterBranchStatus, isLoading: masterBranchStatusIsLoading } = useBranchStatusQuery( org, app, - 'master' + 'master', ); const latestRelease: AppRelease = releases && releases[0] ? releases[0] : null; diff --git a/frontend/app-development/hooks/queries/useAppDeploymentsQuery.ts b/frontend/app-development/hooks/queries/useAppDeploymentsQuery.ts index eaece342a2c..2a237648877 100644 --- a/frontend/app-development/hooks/queries/useAppDeploymentsQuery.ts +++ b/frontend/app-development/hooks/queries/useAppDeploymentsQuery.ts @@ -4,12 +4,18 @@ import { useServicesContext } from 'app-shared/contexts/ServicesContext'; import { AppDeployment } from 'app-shared/types/AppDeployment'; import { QueryKey } from 'app-shared/types/QueryKey'; import { DEPLOYMENTS_REFETCH_INTERVAL } from 'app-shared/constants'; +import { QueryMeta } from '@tanstack/react-query/build/lib'; -export const useAppDeploymentsQuery = (owner, app): UseQueryResult => { +export const useAppDeploymentsQuery = ( + owner, + app, + meta?: QueryMeta, +): UseQueryResult => { const { getDeployments } = useServicesContext(); return useQuery({ queryKey: [QueryKey.AppDeployments, owner, app], queryFn: () => getDeployments(owner, app).then((res) => res.results), refetchInterval: DEPLOYMENTS_REFETCH_INTERVAL, + meta, }); }; diff --git a/frontend/app-development/hooks/queries/useEnvironmentsQuery.ts b/frontend/app-development/hooks/queries/useEnvironmentsQuery.ts index 0e871959774..f1a58ba6fb8 100644 --- a/frontend/app-development/hooks/queries/useEnvironmentsQuery.ts +++ b/frontend/app-development/hooks/queries/useEnvironmentsQuery.ts @@ -2,8 +2,9 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; import { useServicesContext } from 'app-shared/contexts/ServicesContext'; import { DeployEnvironment } from 'app-shared/types/DeployEnvironment'; import { QueryKey } from 'app-shared/types/QueryKey'; +import { QueryMeta } from '@tanstack/react-query/build/lib'; -export const useEnvironmentsQuery = (): UseQueryResult => { +export const useEnvironmentsQuery = (meta?: QueryMeta): UseQueryResult => { const { getEnvironments } = useServicesContext(); - return useQuery([QueryKey.Environments], () => getEnvironments()); + return useQuery([QueryKey.Environments], () => getEnvironments(), { meta }); }; diff --git a/frontend/app-development/hooks/queries/useOrgListQuery.ts b/frontend/app-development/hooks/queries/useOrgListQuery.ts index b83ecfcfefd..7f86a9ea77c 100644 --- a/frontend/app-development/hooks/queries/useOrgListQuery.ts +++ b/frontend/app-development/hooks/queries/useOrgListQuery.ts @@ -2,8 +2,9 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; import { useServicesContext } from 'app-shared/contexts/ServicesContext'; import { QueryKey } from 'app-shared/types/QueryKey'; import { OrgsState } from 'app-shared/types/OrgsState'; +import { QueryMeta } from '@tanstack/react-query/build/lib'; -export const useOrgListQuery = (): UseQueryResult => { +export const useOrgListQuery = (meta?: QueryMeta): UseQueryResult => { const { getOrgList } = useServicesContext(); - return useQuery([QueryKey.OrgList], () => getOrgList()); + return useQuery([QueryKey.OrgList], () => getOrgList(), { meta }); }; diff --git a/frontend/app-development/package.json b/frontend/app-development/package.json index 07680d2119a..7e74257e8a1 100644 --- a/frontend/app-development/package.json +++ b/frontend/app-development/package.json @@ -16,7 +16,6 @@ "classnames": "2.3.2", "history": "5.3.0", "i18next": "23.5.1", - "moment": "2.29.4", "react": "18.2.0", "react-dom": "18.2.0", "react-i18next": "13.2.2", diff --git a/frontend/app-development/test/testUtils.tsx b/frontend/app-development/test/testUtils.tsx index d3564906b4d..14f36ce40ff 100644 --- a/frontend/app-development/test/testUtils.tsx +++ b/frontend/app-development/test/testUtils.tsx @@ -8,12 +8,15 @@ import type { AppStore, RootState } from '../store'; import { setupStore } from '../store'; import { APP_DEVELOPMENT_BASENAME } from 'app-shared/constants'; import { ServicesContextProps, ServicesContextProvider } from 'app-shared/contexts/ServicesContext'; +import { queryClientConfigMock } from 'app-shared/mocks/queryClientMock'; +import { QueryClient } from '@tanstack/react-query/build/lib'; interface ExtendedRenderOptions extends Omit { preloadedState?: PreloadedState; store?: AppStore; startUrl?: string; queries?: Partial; + queryClient?: QueryClient; } export const renderWithProviders = ( @@ -21,16 +24,21 @@ export const renderWithProviders = ( { preloadedState = {}, queries = {}, + queryClient, store = setupStore(preloadedState), startUrl = undefined, ...renderOptions - }: ExtendedRenderOptions = {} + }: ExtendedRenderOptions = {}, ) => { function Wrapper({ children }: React.PropsWithChildren) { return ( - + diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 48d9a217ee0..13ce960b040 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -13,7 +13,13 @@ "access_control.test_what_header": "Hva kan du teste i Altinn Studio?", "address_component.validation_error_house_number": "Bolignummer er ugyldig", "address_component.validation_error_zipcode": "Postnummer er ugyldig", + "administration.activity": "Aktivitet", "administration.administration": "Om appen", + "administration.app_environments_error": "Kunne ikke laste inn statusen for denne applikasjonen. Prøv igjen senere.", + "administration.app_logs_created": "Opprettet {createdDateTime} av {createdBy}", + "administration.app_logs_error": "Kunne ikke laste inn aktiviteten for denne applikasjonen. Prøv igjen senere.", + "administration.app_logs_title": "Versjon {tagName} {environment}, {envName}", + "administration.app_status_error": "Kunne ikke laste inn statusen for {{envName}}-miljøet. Prøv igjen senere.", "administration.copied_app_header": "Velkommen til din kopierte app", "administration.copied_app_information": "Før du setter igang med utviklingen er det noen små justeringer som må gjøres. Les mer om disse endringene i vår <0 href=\"https://docs.altinn.studio/nb/app/getting-started/copy-app/\">brukerdokumentasjon.", "administration.created_by": "Opprettet av:", @@ -30,7 +36,12 @@ "administration.download_repo_heading": "Last ned alle endringer?", "administration.download_repo_info": "Hvis ting henger seg opp, og push ikke virker, kan det være mulig å \"redde\" endringene dine ved å laste ned en zip fil, og manuelt pakke den ut over et lokalt git repo og sjekke inn endrinene.", "administration.fetch_title_error_message": "Kunne ikke laste inn tittelen for denne appen. Prøv igjen senere.", + "administration.go_to_build_log": "Gå til <0>Byggloggen.", + "administration.go_to_publish": "Gå til <0>Publiser.", + "administration.last_published": "Sist publisert {{lastPublishedDate}}.", "administration.navigation_title": "Bygg applikasjonen med våre verktøy", + "administration.no_activity": "Ingen aktivitet eller versjoner er publisert", + "administration.no_app": "Applikasjonen er ikke publisert i miljøet.", "administration.repo_owner_is": "Opprettet for:", "administration.reset_repo_button": "Slett mine endringer", "administration.reset_repo_completed": "Alle endringer er nå slettet", @@ -49,6 +60,8 @@ "administration.service_name_empty_message": "App-navn må fylles ut. Navnet kan endres helt frem til publisering.", "administration.service_owner_is": "Denne appen er opprettet for:", "administration.service_saved_name_administration_description": "Benyttes (sammen med eier) som unik identifikator for appen både i URLer og APIer. Kan ikke endres etter at appen er satt i produksjon.", + "administration.success": "Applikasjonen er tilgjengelig i miljøet.", + "administration.unavailable": "Applikasjonen er midlertidig utilgjengelig i miljøet.", "api_errors.GT_01": "Deling av endringer mislyktes. Vennligst prøv igjen.", "api_errors.ResourceNotFound": "Fant ikke en fil applikasjonen din prøvde å få tak i.", "app_create_release.application_builds_based_on": "Applikasjonen bygges basert på", @@ -266,6 +279,8 @@ "general.create_new": "Lag ny", "general.customer_service_phone_number": "+47 75 00 60 00", "general.dataModel": "datamodell", + "general.date": "Dato", + "general.date_time_format": "{{date}} kl. {{time}}", "general.delete": "Slett", "general.details": "Detaljer", "general.disabled": "Deaktivert", @@ -287,6 +302,7 @@ "general.options": "Alternativer", "general.page": "Side", "general.preview": "Forhåndsvisning", + "general.production": "Produksjon", "general.profile_icon": "Profilikon", "general.required": "Obligatorisk", "general.save": "Lagre", @@ -302,6 +318,7 @@ "general.sign_out": "Logg ut", "general.submit": "Send inn", "general.text": "Tekst", + "general.time_prefix": "kl.", "general.true": "Sann", "general.try_again": "Prøv igjen", "general.unknown_error": "Ukjent feil oppstod ved innlasting av data.", @@ -1506,4 +1523,4 @@ "validation_errors.pattern": "Feil format eller verdi", "validation_errors.required": "Feltet er påkrevd", "validation_errors.value_as_url": "Ugyldig lenke" -} +} \ No newline at end of file diff --git a/frontend/packages/schema-editor/src/components/TypesInspector.tsx b/frontend/packages/schema-editor/src/components/TypesInspector.tsx index 509e9beb7f6..a632f9e658a 100644 --- a/frontend/packages/schema-editor/src/components/TypesInspector.tsx +++ b/frontend/packages/schema-editor/src/components/TypesInspector.tsx @@ -29,7 +29,7 @@ export const TypesInspector = ({ schemaItems }: TypesInspectorProps) => { callback: (newPointer) => { dispatch(setSelectedAndFocusedNode(newPointer)); }, - }) + }), ); }; diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index d3965ec4264..46ec63ceb48 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -9,8 +9,7 @@ export type SupportedFeatureFlags = | 'expressions' | 'settingsModal' | 'processEditor' - | 'configureLayoutSet' - | 'newAdministration'; + | 'configureLayoutSet'; /* * Please add all the features that you want to be toggle on by default here. diff --git a/frontend/packages/text-editor/src/Variables.tsx b/frontend/packages/text-editor/src/Variables.tsx index 23894d4965f..6f30b852acf 100644 --- a/frontend/packages/text-editor/src/Variables.tsx +++ b/frontend/packages/text-editor/src/Variables.tsx @@ -34,7 +34,9 @@ export const Variables = ({ variables }: VariablesProps) => { } variant='tertiary' size='small' />} + trigger={ +
-
- setModalOpen(false)} - handleOpen={() => setModalOpen(true)} - /> -
+ +
+
+ {t('right_menu.rules_conditional_rendering')} +
+
+ setModalOpen(false)} + handleOpen={() => setModalOpen(true)} + />
- - + + + ); }; diff --git a/frontend/packages/ux-editor/src/components/config/Expressions/ExpressionContent.tsx b/frontend/packages/ux-editor/src/components/config/Expressions/ExpressionContent.tsx index c256da4a253..b4c27b2ac47 100644 --- a/frontend/packages/ux-editor/src/components/config/Expressions/ExpressionContent.tsx +++ b/frontend/packages/ux-editor/src/components/config/Expressions/ExpressionContent.tsx @@ -19,7 +19,7 @@ import { tryParseExpression, updateComplexExpression, updateExpression, - updateOperator + updateOperator, } from '../../../utils/expressionsUtils'; import { useText } from '../../../hooks'; import { ComplexExpression } from './ComplexExpression'; @@ -30,7 +30,10 @@ import { stringifyData } from '../../../utils/jsonUtils'; export interface ExpressionContentProps { componentName: string; expression: Expression; - onGetProperties: (expression: Expression) => { availableProperties: string[], expressionProperties: string[] }; + onGetProperties: (expression: Expression) => { + availableProperties: string[]; + expressionProperties: string[]; + }; showRemoveExpressionButton: boolean; onSaveExpression: (expression: Expression) => void; successfullyAddedExpression: boolean; @@ -57,20 +60,22 @@ export const ExpressionContent = ({ const [freeStyleEditing, setFreeStyleEditing] = useState(!!expression.complexExpression); const t = useText(); - const allowToSpecifyExpression = Object.values(onGetProperties(expression).expressionProperties).includes(expression.property); - const allowToSaveExpression = ( - expression.subExpressions?.filter(subExp => !subExp.function)?.length === 0 - && expression.subExpressions.length !== 0 - && expressionInEditMode - && !!expression.property - ) || ( - complexExpressionIsSet(expression.complexExpression) - && expressionInEditMode - && !!expression.property - ); + const allowToSpecifyExpression = Object.values( + onGetProperties(expression).expressionProperties, + ).includes(expression.property); + const allowToSaveExpression = + (expression.subExpressions?.filter((subExp) => !subExp.function)?.length === 0 && + expression.subExpressions.length !== 0 && + expressionInEditMode && + !!expression.property) || + (complexExpressionIsSet(expression.complexExpression) && + expressionInEditMode && + !!expression.property); const propertiesList = onGetProperties(expression).availableProperties; const externalExpression = convertInternalExpressionToExternal(expression); - const isStudioFriendly = isStudioFriendlyExpression(tryParseExpression(expression, externalExpression).complexExpression); + const isStudioFriendly = isStudioFriendlyExpression( + tryParseExpression(expression, externalExpression).complexExpression, + ); const addPropertyToExpression = (property: string) => { const newExpression: Expression = addProperty(expression, property); @@ -112,11 +117,11 @@ export const ExpressionContent = ({ {expressionInEditMode ? (
{t('right_menu.expression_enable_free_style_editing')} @@ -125,14 +130,14 @@ export const ExpressionContent = ({ }} + components={{ bold: }} />

{showRemoveExpressionButton && ( + > + {t('general.save')} + )}
) : ( @@ -182,15 +197,13 @@ export const ExpressionContent = ({ }} + components={{ bold: }} /> {complexExpressionIsSet(expression.complexExpression) ? ( - + ) : ( - + )} {successfullyAddedExpression && (
@@ -203,14 +216,14 @@ export const ExpressionContent = ({
)} ); diff --git a/frontend/packages/ux-editor/src/components/toolbar/ToolbarItemComponent.tsx b/frontend/packages/ux-editor/src/components/toolbar/ToolbarItemComponent.tsx index 3876df1d4a3..6ac9bd4de40 100644 --- a/frontend/packages/ux-editor/src/components/toolbar/ToolbarItemComponent.tsx +++ b/frontend/packages/ux-editor/src/components/toolbar/ToolbarItemComponent.tsx @@ -17,9 +17,7 @@ export const ToolbarItemComponent = (props: IToolbarItemProvidedProps) => { const { t } = useTranslation(); return (
-
- {props.icon && ()} -
+
{props.icon && }
{props.thirdPartyLabel == null ? getComponentTitleByComponentType(props.componentType, t) diff --git a/yarn.lock b/yarn.lock index 554d8b438a8..3b258982ed5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5621,7 +5621,6 @@ __metadata: history: 5.3.0 i18next: 23.5.1 jest: 29.7.0 - moment: 2.29.4 react: 18.2.0 react-dom: 18.2.0 react-i18next: 13.2.2 @@ -12231,13 +12230,6 @@ __metadata: languageName: node linkType: hard -"moment@npm:2.29.4": - version: 2.29.4 - resolution: "moment@npm:2.29.4" - checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e - languageName: node - linkType: hard - "morgan@npm:1.10.0": version: 1.10.0 resolution: "morgan@npm:1.10.0"