diff --git a/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js b/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js new file mode 100644 index 00000000..bedddbf4 --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/application-auth-clients-page.js @@ -0,0 +1,40 @@ +import { expect } from '@playwright/test'; + +const { AuthenticatedPage } = require('../authenticated-page'); + +export class AdminApplicationAuthClientsPage extends AuthenticatedPage { + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + + this.authClientsTab = this.page.getByText('AUTH CLIENTS'); + this.saveButton = this.page.getByTestId('submitButton'); + this.successSnackbar = this.page.getByTestId('snackbar-save-admin-apps-settings-success'); + this.createFirstAuthClientButton = this.page.getByTestId('no-results'); + this.createAuthClientButton = this.page.getByTestId('create-auth-client-button'); + this.submitAuthClientFormButton = this.page.getByTestId('submit-auth-client-form'); + this.authClientEntry = this.page.getByTestId('auth-client'); + } + + async openAuthClientsTab() { + this.authClientsTab.click(); + } + + async openFirstAuthClientCreateForm() { + this.createFirstAuthClientButton.click(); + } + + async openAuthClientCreateForm() { + this.createAuthClientButton.click(); + } + + async submitAuthClientForm() { + this.submitAuthClientFormButton.click(); + } + + async authClientShouldBeVisible(authClientName) { + await expect(this.authClientEntry.filter({ hasText: authClientName })).toBeVisible(); + } +} diff --git a/packages/e2e-tests/fixtures/admin/application-settings-page.js b/packages/e2e-tests/fixtures/admin/application-settings-page.js new file mode 100644 index 00000000..37dda591 --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/application-settings-page.js @@ -0,0 +1,59 @@ +const { AuthenticatedPage } = require('../authenticated-page'); +const { expect } = require('@playwright/test'); + +export class AdminApplicationSettingsPage extends AuthenticatedPage { + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + + this.allowCustomConnectionsSwitch = this.page.locator('[name="allowCustomConnection"]'); + this.allowSharedConnectionsSwitch = this.page.locator('[name="shared"]'); + this.disableConnectionsSwitch = this.page.locator('[name="disabled"]'); + this.saveButton = this.page.getByTestId('submit-button'); + this.successSnackbar = this.page.getByTestId('snackbar-save-admin-apps-settings-success'); + } + + async allowCustomConnections() { + await expect(this.disableConnectionsSwitch).not.toBeChecked(); + await this.allowCustomConnectionsSwitch.check(); + await this.saveButton.click(); + } + + async allowSharedConnections() { + await expect(this.disableConnectionsSwitch).not.toBeChecked(); + await this.allowSharedConnectionsSwitch.check(); + await this.saveButton.click(); + } + + async disallowConnections() { + await expect(this.disableConnectionsSwitch).not.toBeChecked(); + await this.disableConnectionsSwitch.check(); + await this.saveButton.click(); + } + + async disallowCustomConnections() { + await expect(this.disableConnectionsSwitch).toBeChecked(); + await this.allowCustomConnectionsSwitch.uncheck(); + await this.saveButton.click(); + } + + async disallowSharedConnections() { + await expect(this.disableConnectionsSwitch).toBeChecked(); + await this.allowSharedConnectionsSwitch.uncheck(); + await this.saveButton.click(); + } + + async allowConnections() { + await expect(this.disableConnectionsSwitch).toBeChecked(); + await this.disableConnectionsSwitch.uncheck(); + await this.saveButton.click(); + } + + async expectSuccessSnackbarToBeVisible() { + await expect(this.successSnackbar).toHaveCount(1); + await this.successSnackbar.click(); + await expect(this.successSnackbar).toHaveCount(0); + } +} diff --git a/packages/e2e-tests/fixtures/admin/applications-page.js b/packages/e2e-tests/fixtures/admin/applications-page.js new file mode 100644 index 00000000..77427c15 --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/applications-page.js @@ -0,0 +1,32 @@ +const { AuthenticatedPage } = require('../authenticated-page'); + +export class AdminApplicationsPage extends AuthenticatedPage { + screenshotPath = '/admin-settings/apps'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + + this.searchInput = page.locator('[id="search-input"]'); + this.appRow = page.getByTestId('app-row'); + this.appsDrawerLink = page.getByTestId('apps-drawer-link'); + this.appsLoader = page.getByTestId('apps-loader'); + } + + async openApplication(appName) { + await this.searchInput.fill(appName); + await this.appRow.locator(this.page.getByText(appName)).click(); + } + + async navigateTo() { + await this.profileMenuButton.click(); + await this.adminMenuItem.click(); + await this.appsDrawerLink.click(); + await this.isMounted(); + await this.appsLoader.waitFor({ + state: 'detached', + }); + } +} diff --git a/packages/e2e-tests/fixtures/admin/index.js b/packages/e2e-tests/fixtures/admin/index.js index fe746243..746c85dd 100644 --- a/packages/e2e-tests/fixtures/admin/index.js +++ b/packages/e2e-tests/fixtures/admin/index.js @@ -6,6 +6,10 @@ const { AdminRolesPage } = require('./roles-page'); const { AdminCreateRolePage } = require('./create-role-page'); const { AdminEditRolePage } = require('./edit-role-page'); +const { AdminApplicationsPage } = require('./applications-page'); +const { AdminApplicationSettingsPage } = require('./application-settings-page'); +const { AdminApplicationAuthClientsPage } = require('./application-auth-clients-page'); + export const adminFixtures = { adminUsersPage: async ({ page }, use) => { await use(new AdminUsersPage(page)); @@ -13,17 +17,26 @@ export const adminFixtures = { adminCreateUserPage: async ({ page }, use) => { await use(new AdminCreateUserPage(page)); }, - adminEditUserPage: async ({page}, use) => { + adminEditUserPage: async ({ page }, use) => { await use(new AdminEditUserPage(page)); }, - adminRolesPage: async ({ page}, use) => { + adminRolesPage: async ({ page }, use) => { await use(new AdminRolesPage(page)); }, - adminEditRolePage: async ({ page}, use) => { + adminEditRolePage: async ({ page }, use) => { await use(new AdminEditRolePage(page)); }, - adminCreateRolePage: async ({ page}, use) => { + adminCreateRolePage: async ({ page }, use) => { await use(new AdminCreateRolePage(page)); }, + adminApplicationsPage: async ({ page }, use) => { + await use(new AdminApplicationsPage(page)); + }, + adminApplicationSettingsPage: async ({ page }, use) => { + await use(new AdminApplicationSettingsPage(page)); + }, + adminApplicationAuthClientsPage: async ({ page }, use) => { + await use(new AdminApplicationAuthClientsPage(page)); + } }; diff --git a/packages/e2e-tests/fixtures/flow-editor-page.js b/packages/e2e-tests/fixtures/flow-editor-page.js index a4db7003..af321b59 100644 --- a/packages/e2e-tests/fixtures/flow-editor-page.js +++ b/packages/e2e-tests/fixtures/flow-editor-page.js @@ -30,6 +30,8 @@ export class FlowEditorPage extends AuthenticatedPage { this.flowNameInput = this.page .getByTestId('editableTypographyInput') .locator('input'); + + this.flowStep = this.page.getByTestId('flow-step'); } async createWebhookTrigger(workSynchronously) { @@ -68,11 +70,11 @@ export class FlowEditorPage extends AuthenticatedPage { } async chooseAppAndEvent(appName, eventName) { + await expect(this.appAutocomplete).toHaveCount(1); await this.appAutocomplete.click(); await this.page.getByRole('option', { name: appName }).click(); await expect(this.eventAutocomplete).toBeVisible(); await this.eventAutocomplete.click(); - await expect(this.page.locator('[data-testid="ErrorIcon"]')).toHaveCount(2); await Promise.all([ this.page.waitForResponse(resp => /(apps\/.*\/actions\/.*\/substeps)/.test(resp.url()) && resp.status() === 200), this.page.getByRole('option', { name: eventName }).click(), diff --git a/packages/e2e-tests/fixtures/postgres-client-config.js b/packages/e2e-tests/fixtures/postgres-config.js similarity index 56% rename from packages/e2e-tests/fixtures/postgres-client-config.js rename to packages/e2e-tests/fixtures/postgres-config.js index 8ebc831d..8d82478c 100644 --- a/packages/e2e-tests/fixtures/postgres-client-config.js +++ b/packages/e2e-tests/fixtures/postgres-config.js @@ -1,6 +1,9 @@ -const { Client } = require('pg'); +const { Pool } = require('pg'); -const client = new Client({ +const pool = new Pool({ + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, host: process.env.POSTGRES_HOST, user: process.env.POSTGRES_USERNAME, port: process.env.POSTGRES_PORT, @@ -8,4 +11,4 @@ const client = new Client({ database: process.env.POSTGRES_DATABASE }); -exports.client = client; +exports.pgPool = pool; diff --git a/packages/e2e-tests/tests/admin/applications.spec.js b/packages/e2e-tests/tests/admin/applications.spec.js new file mode 100644 index 00000000..2fad49b9 --- /dev/null +++ b/packages/e2e-tests/tests/admin/applications.spec.js @@ -0,0 +1,231 @@ +const { test, expect } = require('../../fixtures/index'); +const { pgPool } = require('../../fixtures/postgres-config'); + +test.describe('Admin Applications', () => { + test.beforeAll(async () => { + const deleteAppAuthClients = { + text: 'DELETE FROM app_auth_clients WHERE app_key in ($1, $2, $3, $4, $5)', + values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'] + }; + + const deleteAppConfigs = { + text: 'DELETE FROM app_configs WHERE key in ($1, $2, $3, $4, $5)', + values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'] + }; + + try { + const deleteAppAuthClientsResult = await pgPool.query(deleteAppAuthClients); + expect(deleteAppAuthClientsResult.command).toBe('DELETE'); + const deleteAppConfigsResult = await pgPool.query(deleteAppConfigs); + expect(deleteAppConfigsResult.command).toBe('DELETE'); + } catch (err) { + console.error(err.message); + throw err; + } + }); + + test.beforeEach(async ({ adminApplicationsPage }) => { + await adminApplicationsPage.navigateTo(); + }); + + test('Admin should be able to toggle Application settings', async ({ + adminApplicationsPage, + adminApplicationSettingsPage, + page + }) => { + await adminApplicationsPage.openApplication('Carbone'); + await expect(page.url()).toContain('/admin-settings/apps/carbone/settings'); + + await adminApplicationSettingsPage.allowCustomConnections(); + await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + await adminApplicationSettingsPage.allowSharedConnections(); + await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + await adminApplicationSettingsPage.disallowConnections(); + await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + + await page.reload(); + + await adminApplicationSettingsPage.disallowCustomConnections(); + await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + await adminApplicationSettingsPage.disallowSharedConnections(); + await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + await adminApplicationSettingsPage.allowConnections(); + await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + }); + + test('should allow only custom connections', async ({ + adminApplicationsPage, + adminApplicationSettingsPage, + flowEditorPage, + page + }) => { + await adminApplicationsPage.openApplication('Spotify'); + await expect(page.url()).toContain('/admin-settings/apps/spotify/settings'); + + await adminApplicationSettingsPage.allowCustomConnections(); + await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + + await page.goto('/'); + await page.getByTestId('create-flow-button').click(); + await page.waitForURL( + /\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/ + ); + + await expect(flowEditorPage.flowStep).toHaveCount(2); + const triggerStep = flowEditorPage.flowStep.last(); + await triggerStep.click(); + + await flowEditorPage.chooseAppAndEvent("Spotify", "Create Playlist"); + await flowEditorPage.connectionAutocomplete.click(); + + const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); + + await expect(newConnectionOption).toBeEnabled(); + await expect(newConnectionOption).toHaveCount(1); + await expect(newSharedConnectionOption).toHaveCount(0); + }); + + test('should allow only shared connections', async ({ + adminApplicationsPage, + adminApplicationSettingsPage, + adminApplicationAuthClientsPage, + flowEditorPage, + page + }) => { + await adminApplicationsPage.openApplication('Reddit'); + await expect(page.url()).toContain('/admin-settings/apps/reddit/settings'); + + await adminApplicationSettingsPage.allowSharedConnections(); + await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + + await adminApplicationAuthClientsPage.openAuthClientsTab(); + await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm(); + const authClientForm = page.getByTestId("auth-client-form"); + await authClientForm.locator(page.getByTestId('switch')).check(); + await authClientForm.locator(page.locator('[name="name"]')).fill('redditAuthClient'); + await authClientForm.locator(page.locator('[name="clientId"]')).fill('redditClientId'); + await authClientForm.locator(page.locator('[name="clientSecret"]')).fill('redditClientSecret'); + await adminApplicationAuthClientsPage.submitAuthClientForm(); + await adminApplicationAuthClientsPage.authClientShouldBeVisible('redditAuthClient'); + + await page.goto('/'); + await page.getByTestId('create-flow-button').click(); + await page.waitForURL( + /\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/ + ); + + await expect(flowEditorPage.flowStep).toHaveCount(2); + const triggerStep = flowEditorPage.flowStep.last(); + await triggerStep.click(); + + await flowEditorPage.chooseAppAndEvent("Reddit", "Create link post"); + await flowEditorPage.connectionAutocomplete.click(); + + const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); + + await expect(newConnectionOption).toHaveCount(0); + await expect(newSharedConnectionOption).toBeEnabled(); + await expect(newSharedConnectionOption).toHaveCount(1); + }); + + test('should not allow any connections', async ({ + adminApplicationsPage, + adminApplicationSettingsPage, + flowEditorPage, + page + }) => { + await adminApplicationsPage.openApplication('DeepL'); + await expect(page.url()).toContain('/admin-settings/apps/deepl/settings'); + + await adminApplicationSettingsPage.disallowConnections(); + await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + + await page.goto('/'); + await page.getByTestId('create-flow-button').click(); + await page.waitForURL( + /\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/ + ); + + await expect(flowEditorPage.flowStep).toHaveCount(2); + const triggerStep = flowEditorPage.flowStep.last(); + await triggerStep.click(); + + await flowEditorPage.chooseAppAndEvent("DeepL", "Translate text"); + await flowEditorPage.connectionAutocomplete.click(); + + const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); + const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' }); + + await expect(noConnectionsOption).toHaveCount(1); + await expect(newConnectionOption).toHaveCount(0); + await expect(newSharedConnectionOption).toHaveCount(0); + }); + + test('should not allow new connections but only already created', async ({ + adminApplicationsPage, + adminApplicationSettingsPage, + flowEditorPage, + page + }) => { + const queryUser = { + text: 'SELECT * FROM users WHERE email = $1', + values: [process.env.LOGIN_EMAIL] + }; + + try { + const queryUserResult = await pgPool.query(queryUser); + expect(queryUserResult.rowCount).toEqual(1); + + const createMailchimpConnection = { + text: 'INSERT INTO connections (key, data, user_id, verified, draft) VALUES ($1, $2, $3, $4, $5)', + values: [ + 'mailchimp', + "U2FsdGVkX1+cAtdHwLiuRL4DaK/T1aljeeKyPMmtWK0AmAIsKhYwQiuyQCYJO3mdZ31z73hqF2Y+yj2Kn2/IIpLRqCxB2sC0rCDCZyolzOZ290YcBXSzYRzRUxhoOcZEtwYDKsy8AHygKK/tkj9uv9k6wOe1LjipNik4VmRhKjEYizzjLrJpbeU1oY+qW0GBpPYomFTeNf+MejSSmsUYyYJ8+E/4GeEfaonvsTSwMT7AId98Lck6Vy4wrfgpm7sZZ8xU15/HqXZNc8UCo2iTdw45xj/Oov9+brX4WUASFPG8aYrK8dl/EdaOvr89P8uIofbSNZ25GjJvVF5ymarrPkTZ7djjJXchzpwBY+7GTJfs3funR/vIk0Hq95jgOFFP1liZyqTXSa49ojG3hzojRQ==", + queryUserResult.rows[0].id, + 'true', + 'false' + ], + }; + + const createMailchimpConnectionResult = await pgPool.query(createMailchimpConnection); + expect(createMailchimpConnectionResult.rowCount).toBe(1); + expect(createMailchimpConnectionResult.command).toBe('INSERT'); + } catch (err) { + console.error(err.message); + throw err; + } + + await adminApplicationsPage.openApplication('Mailchimp'); + await expect(page.url()).toContain('/admin-settings/apps/mailchimp/settings'); + + await adminApplicationSettingsPage.disallowConnections(); + await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible(); + + await page.goto('/'); + await page.getByTestId('create-flow-button').click(); + await page.waitForURL( + /\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/ + ); + + await expect(flowEditorPage.flowStep).toHaveCount(2); + const triggerStep = flowEditorPage.flowStep.last(); + await triggerStep.click(); + + await flowEditorPage.chooseAppAndEvent("Mailchimp", "Create campaign"); + await flowEditorPage.connectionAutocomplete.click(); + await expect(page.getByRole('option').first()).toHaveText('Unnamed'); + + const existingConnection = page.getByRole('option').filter({ hasText: 'Unnamed' }); + const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' }); + const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' }); + const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' }); + + await expect(await existingConnection.count()).toBeGreaterThan(0); + await expect(noConnectionsOption).toHaveCount(0); + await expect(newConnectionOption).toHaveCount(0); + await expect(newSharedConnectionOption).toHaveCount(0); + }); +}); diff --git a/packages/e2e-tests/tests/global.teardown.js b/packages/e2e-tests/tests/global.teardown.js index ada10ffc..20bc97aa 100644 --- a/packages/e2e-tests/tests/global.teardown.js +++ b/packages/e2e-tests/tests/global.teardown.js @@ -3,10 +3,10 @@ import knex from 'knex'; import knexConfig from '../knexfile.js'; publicTest.describe('restore db', () => { - publicTest('clean db and perform migrations', async () => { - const knexClient = knex(knexConfig) - const migrator = knexClient.migrate; - await migrator.rollback({}, true); - await migrator.latest(); - }) + publicTest('clean db and perform migrations', async () => { + const knexClient = knex(knexConfig); + const migrator = knexClient.migrate; + await migrator.rollback({}, true); + await migrator.latest(); + }); }); diff --git a/packages/e2e-tests/tests/user-invitation/invitation.spec.js b/packages/e2e-tests/tests/user-invitation/invitation.spec.js index 33bc4378..2035f808 100644 --- a/packages/e2e-tests/tests/user-invitation/invitation.spec.js +++ b/packages/e2e-tests/tests/user-invitation/invitation.spec.js @@ -1,5 +1,5 @@ const { publicTest, expect } = require('../../fixtures/index'); -const { client } = require('../../fixtures/postgres-client-config'); +const { pgPool } = require('../../fixtures/postgres-config'); const { DateTime } = require('luxon'); publicTest.describe('Accept invitation page', () => { @@ -17,17 +17,9 @@ publicTest.describe('Accept invitation page', () => { }); publicTest.describe('Accept invitation page - users', () => { - const expiredTokenDate = DateTime.now().minus({days: 3}).toISO(); + const expiredTokenDate = DateTime.now().minus({ days: 3 }).toISO(); const token = (Math.random() + 1).toString(36).substring(2); - publicTest.beforeAll(async () => { - await client.connect(); - }); - - publicTest.afterAll(async () => { - await client.end(); - }); - publicTest('should not be able to set the password if token is expired', async ({ acceptInvitationPage, adminCreateUserPage }) => { adminCreateUserPage.seed(Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)); const user = adminCreateUserPage.generateUser(); @@ -38,7 +30,7 @@ publicTest.describe('Accept invitation page', () => { }; try { - const queryRoleIdResult = await client.query(queryRole); + const queryRoleIdResult = await pgPool.query(queryRole); expect(queryRoleIdResult.rowCount).toEqual(1); const insertUser = { @@ -46,7 +38,7 @@ publicTest.describe('Accept invitation page', () => { values: [user.email, user.fullName, queryRoleIdResult.rows[0].id, 'invited', token, expiredTokenDate], }; - const insertUserResult = await client.query(insertUser); + const insertUserResult = await pgPool.query(insertUser); expect(insertUserResult.rowCount).toBe(1); expect(insertUserResult.command).toBe('INSERT'); } catch (err) { @@ -68,7 +60,7 @@ publicTest.describe('Accept invitation page', () => { }; try { - const queryRoleIdResult = await client.query(queryRole); + const queryRoleIdResult = await pgPool.query(queryRole); expect(queryRoleIdResult.rowCount).toEqual(1); const insertUser = { @@ -76,7 +68,7 @@ publicTest.describe('Accept invitation page', () => { values: [user.email, user.fullName, dateNow, queryRoleIdResult.rows[0].id, 'invited', token, dateNow], }; - const insertUserResult = await client.query(insertUser); + const insertUserResult = await pgPool.query(insertUser); expect(insertUserResult.rowCount).toBe(1); expect(insertUserResult.command).toBe('INSERT'); } catch (err) { diff --git a/packages/web/src/components/AdminApplicationAuthClientDialog/index.jsx b/packages/web/src/components/AdminApplicationAuthClientDialog/index.jsx index 2fc93069..9172bc1b 100644 --- a/packages/web/src/components/AdminApplicationAuthClientDialog/index.jsx +++ b/packages/web/src/components/AdminApplicationAuthClientDialog/index.jsx @@ -49,6 +49,7 @@ function AdminApplicationAuthClientDialog(props) { ) : (
( @@ -67,6 +68,7 @@ function AdminApplicationAuthClientDialog(props) { ))} {sortedAuthClients.map((client) => ( - + - diff --git a/packages/web/src/components/AdminApplicationSettings/index.jsx b/packages/web/src/components/AdminApplicationSettings/index.jsx index 8022d50f..b24d2557 100644 --- a/packages/web/src/components/AdminApplicationSettings/index.jsx +++ b/packages/web/src/components/AdminApplicationSettings/index.jsx @@ -87,6 +87,7 @@ function AdminApplicationSettings(props) { + {!!to && } diff --git a/packages/web/src/components/Switch/index.jsx b/packages/web/src/components/Switch/index.jsx index f98689bd..bc6d8ca0 100644 --- a/packages/web/src/components/Switch/index.jsx +++ b/packages/web/src/components/Switch/index.jsx @@ -42,6 +42,7 @@ function Switch(props) { {...FormControlLabelProps} control={