From 424b4c2378397ea427d771016b50f059d018074a Mon Sep 17 00:00:00 2001 From: Geido <60598000+geido@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:23:32 +0100 Subject: [PATCH] chore: Add Embed Modal extension override and tests (#26759) --- .../src/ui-overrides/types.ts | 10 + .../EmbeddedModal/EmbeddedModal.test.tsx | 172 ++++++++++++++++++ .../index.tsx} | 13 +- .../src/dashboard/components/Header/index.jsx | 2 +- 4 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/EmbeddedModal/EmbeddedModal.test.tsx rename superset-frontend/src/dashboard/components/{DashboardEmbedControls.tsx => EmbeddedModal/index.tsx} (94%) diff --git a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts index 27646442de3d9..45ec06e90ed7d 100644 --- a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts +++ b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts @@ -135,11 +135,21 @@ export interface SliceHeaderExtension { dashboardId: number; } +/** + * Interface for extensions to Embed Modal + */ +export interface DashboardEmbedModalExtensions { + dashboardId: string; + show: boolean; + onHide: () => void; +} + export type Extensions = Partial<{ 'alertsreports.header.icon': React.ComponentType; 'embedded.documentation.configuration_details': React.ComponentType; 'embedded.documentation.description': ReturningDisplayable; 'embedded.documentation.url': string; + 'embedded.modal': React.ComponentType; 'dashboard.nav.right': React.ComponentType; 'navbar.right-menu.item.icon': React.ComponentType; 'navbar.right': React.ComponentType; diff --git a/superset-frontend/src/dashboard/components/EmbeddedModal/EmbeddedModal.test.tsx b/superset-frontend/src/dashboard/components/EmbeddedModal/EmbeddedModal.test.tsx new file mode 100644 index 0000000000000..a33115bf36c8f --- /dev/null +++ b/superset-frontend/src/dashboard/components/EmbeddedModal/EmbeddedModal.test.tsx @@ -0,0 +1,172 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { + render, + screen, + fireEvent, + waitFor, +} from 'spec/helpers/testing-library'; +import '@testing-library/jest-dom'; +import { + SupersetApiError, + getExtensionsRegistry, + makeApi, +} from '@superset-ui/core'; +import setupExtensions from 'src/setup/setupExtensions'; +import DashboardEmbedModal from './index'; + +const defaultResponse = { + result: { uuid: 'uuid', dashboard_id: '1', allowed_domains: ['example.com'] }, +}; + +jest.mock('@superset-ui/core', () => ({ + ...jest.requireActual('@superset-ui/core'), + makeApi: jest.fn(), +})); + +const mockOnHide = jest.fn(); +const defaultProps = { + dashboardId: '1', + show: true, + onHide: mockOnHide, +}; +const resetMockApi = () => { + (makeApi as any).mockReturnValue( + jest.fn().mockResolvedValue(defaultResponse), + ); +}; +const setMockApiNotFound = () => { + const notFound = new SupersetApiError({ message: 'Not found', status: 404 }); + (makeApi as any).mockReturnValue(jest.fn().mockRejectedValue(notFound)); +}; + +const setup = () => { + render(, { useRedux: true }); + resetMockApi(); +}; + +beforeEach(() => { + jest.clearAllMocks(); + resetMockApi(); +}); + +test('renders', async () => { + setup(); + expect(await screen.findByText('Embed')).toBeInTheDocument(); +}); + +test('renders loading state', async () => { + setup(); + await waitFor(() => { + expect(screen.getByRole('status', { name: 'Loading' })).toBeInTheDocument(); + }); +}); + +test('renders the modal default content', async () => { + render(, { useRedux: true }); + expect(await screen.findByText('Settings')).toBeInTheDocument(); + expect( + screen.getByText(new RegExp(/Allowed Domains/, 'i')), + ).toBeInTheDocument(); +}); + +test('renders the correct actions when dashboard is ready to embed', async () => { + setup(); + expect( + await screen.findByRole('button', { name: 'Deactivate' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Save changes' }), + ).toBeInTheDocument(); +}); + +test('renders the correct actions when dashboard is not ready to embed', async () => { + setMockApiNotFound(); + setup(); + expect( + await screen.findByRole('button', { name: 'Enable embedding' }), + ).toBeInTheDocument(); +}); + +test('enables embedding', async () => { + setMockApiNotFound(); + setup(); + + const enableEmbed = await screen.findByRole('button', { + name: 'Enable embedding', + }); + expect(enableEmbed).toBeInTheDocument(); + + fireEvent.click(enableEmbed); + + expect( + await screen.findByRole('button', { name: 'Deactivate' }), + ).toBeInTheDocument(); +}); + +test('shows and hides the confirmation modal on deactivation', async () => { + setup(); + + const deactivate = await screen.findByRole('button', { name: 'Deactivate' }); + fireEvent.click(deactivate); + + expect(await screen.findByText('Disable embedding?')).toBeInTheDocument(); + expect( + screen.getByText('This will remove your current embed configuration.'), + ).toBeInTheDocument(); + + const okBtn = screen.getByRole('button', { name: 'OK' }); + fireEvent.click(okBtn); + + await waitFor(() => { + expect(screen.queryByText('Disable embedding?')).not.toBeInTheDocument(); + }); +}); + +test('enables the "Save Changes" button', async () => { + setup(); + + const allowedDomainsInput = await screen.findByLabelText( + new RegExp(/Allowed Domains/, 'i'), + ); + const saveChangesBtn = screen.getByRole('button', { name: 'Save changes' }); + + expect(saveChangesBtn).toBeDisabled(); + expect(allowedDomainsInput).toBeInTheDocument(); + + fireEvent.change(allowedDomainsInput, { target: { value: 'test.com' } }); + expect(saveChangesBtn).toBeEnabled(); +}); + +test('adds extension to DashboardEmbedModal', async () => { + const extensionsRegistry = getExtensionsRegistry(); + + extensionsRegistry.set('embedded.modal', () => ( + <>dashboard.embed.modal.extension component + )); + + setupExtensions(); + setup(); + + expect( + await screen.findByText('dashboard.embed.modal.extension component'), + ).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/dashboard/components/DashboardEmbedControls.tsx b/superset-frontend/src/dashboard/components/EmbeddedModal/index.tsx similarity index 94% rename from superset-frontend/src/dashboard/components/DashboardEmbedControls.tsx rename to superset-frontend/src/dashboard/components/EmbeddedModal/index.tsx index 8d7702edd4d3d..218a9d37a7b5c 100644 --- a/superset-frontend/src/dashboard/components/DashboardEmbedControls.tsx +++ b/superset-frontend/src/dashboard/components/EmbeddedModal/index.tsx @@ -31,7 +31,7 @@ import Button from 'src/components/Button'; import { Input } from 'src/components/Input'; import { useToasts } from 'src/components/MessageToasts/withToasts'; import { FormItem } from 'src/components/Form'; -import { EmbeddedDashboard } from '../types'; +import { EmbeddedDashboard } from 'src/dashboard/types'; const extensionsRegistry = getExtensionsRegistry(); @@ -135,6 +135,7 @@ export const DashboardEmbedControls = ({ dashboardId, onHide }: Props) => { // 404 just means the dashboard isn't currently embedded return { result: null }; } + addDangerToast(t('Sorry, something went wrong. Please try again.')); throw err; }) .then(({ result }) => { @@ -199,6 +200,7 @@ export const DashboardEmbedControls = ({ dashboardId, onHide }: Props) => { setAllowedDomains(event.target.value)} @@ -237,12 +239,17 @@ export const DashboardEmbedControls = ({ dashboardId, onHide }: Props) => { ); }; -export const DashboardEmbedModal = (props: Props) => { +const DashboardEmbedModal = (props: Props) => { const { show, onHide } = props; + const DashboardEmbedModalExtension = extensionsRegistry.get('embedded.modal'); - return ( + return DashboardEmbedModalExtension ? ( + + ) : ( ); }; + +export default DashboardEmbedModal; diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index 7d96ad6b54590..e94aa610a1557 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -55,7 +55,7 @@ import setPeriodicRunner, { stopPeriodicRender, } from 'src/dashboard/util/setPeriodicRunner'; import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions'; -import { DashboardEmbedModal } from '../DashboardEmbedControls'; +import DashboardEmbedModal from '../EmbeddedModal'; import OverwriteConfirm from '../OverwriteConfirm'; const extensionsRegistry = getExtensionsRegistry();