mikebridge commented on code in PR #45649:
URL: https://github.com/apache/superset/pull/45649#discussion_r4407242986
##########
superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx:
##########
@@ -425,3 +432,301 @@ test('user with can_duplicate sees duplicate button only
for virtual datasets',
const virtualDuplicateButton = within(virtualRow!).getByTestId('copy');
expect(virtualDuplicateButton).toBeInTheDocument();
});
+
+describe('semantic layer setup transition', () => {
+ const configure = (canWriteLayer: boolean) => {
+ jest
+ .mocked(isFeatureEnabled)
+ .mockImplementation(flag => flag === 'SEMANTIC_LAYERS');
+ setupApiPermissions(['can_read', 'can_write']);
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/',
+ { result: [] },
+ { name: 'setup-layers' },
+ );
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/types',
+ { result: [] },
+ { name: 'setup-types' },
+ );
+ return {
+ ...mockAdminUser,
+ roles: {
+ Custom: [
+ ['can_read', 'SemanticLayer'],
+ ...(canWriteLayer ? [['can_write', 'SemanticLayer']] : []),
+ ],
+ },
+ };
+ };
+
+ const openView = async () => {
+ await userEvent.click(await screen.findByTestId('btn-create-new'));
+ await userEvent.click(
+ await screen.findByText('Semantic View', {
+ selector: '.ant-dropdown-menu-title-content',
+ }),
+ );
+ };
+
+ test('layer writers can switch dialogs and cancel back to a fresh layer
list', async () => {
+ renderDatasetList(configure(true));
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ expect(
+ await screen.findByText(
+ 'No semantic layer types are available. Ask your administrator to
check the semantic layer setup for this deployment.',
+ ),
+ ).toBeInTheDocument();
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ fireEvent.keyDown(screen.getByRole('dialog'), {
+ key: 'Escape',
+ keyCode: 27,
+ });
+ expect(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ ).toBeInTheDocument();
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ expect(fetchMock.callHistory.calls('setup-layers')).toHaveLength(2);
+ expect(
+ fetchMock.callHistory
+ .calls()
+ .filter(call => call.options.method === 'POST'),
+ ).toHaveLength(0);
+ await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+ await waitFor(() =>
+ expect(screen.getByTestId('btn-create-new')).toHaveFocus(),
+ );
+ });
+
+ test('dataset writers without layer-write permission get administrator
guidance', async () => {
+ renderDatasetList(configure(false));
+ await openView();
+ expect(
+ await screen.findByText(
+ /Ask your administrator to set up a semantic layer or grant you
access/,
+ ),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Create semantic layer' }),
+ ).not.toBeInTheDocument();
+ });
+
+ test.each([true, false])(
+ 'creation success=%s returns only after a successful save',
+ async succeeds => {
+ const user = configure(true);
+ fetchMock.removeRoutes({ names: ['setup-types'] });
+ fetchMock.get('glob:*/api/v1/semantic_layer/types', {
+ result: [{ id: 'example', name: 'Example provider' }],
+ });
+ fetchMock.post('glob:*/api/v1/semantic_layer/schema/configuration', {
+ result: { type: 'object', properties: {} },
+ });
+ fetchMock.post(
+ 'glob:*/api/v1/semantic_layer/',
+ succeeds ? { result: { uuid: 'new-layer' } } : 500,
+ { name: 'create-layer' },
+ );
+ renderDatasetList(user);
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ const typePicker = await screen.findByRole('combobox', {
+ name: 'Semantic layer type',
+ });
+ await waitFor(() => expect(typePicker).toBeEnabled());
+ fireEvent.mouseDown(typePicker);
+ await userEvent.click(
+ await screen.findByText('Example provider', {
+ selector: '.ant-select-item-option-content',
+ }),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Add' }));
+ const create = await screen.findByRole('button', { name: 'Create' });
+ await waitFor(() => expect(create).toBeEnabled());
+ fetchMock.removeRoutes({ names: ['setup-layers'] });
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/',
+ { result: [{ uuid: 'new-layer', name: 'Example provider' }] },
+ { name: 'refreshed-layers' },
+ );
+ await userEvent.click(create);
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('create-layer')).toHaveLength(1),
+ );
+ if (succeeds) {
+ expect(
+ await screen.findByRole('combobox', { name: 'Semantic layer' }),
+ ).toHaveValue('');
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('refreshed-layers')).toHaveLength(
+ 1,
+ ),
+ );
+ expect(screen.getByRole('button', { name: 'Add' })).toBeDisabled();
+ } else {
+ await waitFor(() => expect(create).toBeEnabled());
+ expect(
+ screen.queryByRole('combobox', { name: 'Semantic layer' }),
+ ).not.toBeInTheDocument();
+
expect(fetchMock.callHistory.calls('refreshed-layers')).toHaveLength(0);
+ }
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ expect(
+ fetchMock.callHistory
+ .calls()
+ .filter(
+ call =>
+ String(call.url).includes('/semantic_view/') &&
+ call.options.method === 'POST',
+ ),
+ ).toHaveLength(0);
+ },
+ );
+
+ test('late layer creation cannot replace a newer setup session', async () =>
{
+ const user = configure(true);
+ fetchMock.removeRoutes({ names: ['setup-types'] });
+ fetchMock.get('glob:*/api/v1/semantic_layer/types', {
+ result: [{ id: 'example', name: 'Example provider' }],
+ });
+ fetchMock.post('glob:*/api/v1/semantic_layer/schema/configuration', {
+ result: { type: 'object', properties: {} },
+ });
+ let finishSave!: (value: { result: { uuid: string } }) => void;
+ fetchMock.post(
+ 'glob:*/api/v1/semantic_layer/',
+ () =>
+ new Promise(resolve => {
+ finishSave = resolve;
+ }),
+ { name: 'pending-create' },
+ );
+ renderDatasetList(user);
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ const typePicker = await screen.findByRole('combobox', {
+ name: 'Semantic layer type',
+ });
+ await waitFor(() => expect(typePicker).toBeEnabled());
+ fireEvent.mouseDown(typePicker);
+ await userEvent.click(
+ await screen.findByText('Example provider', {
+ selector: '.ant-select-item-option-content',
+ }),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Add' }));
+ const create = await screen.findByRole('button', { name: 'Create' });
+ await waitFor(() => expect(create).toBeEnabled());
+ await userEvent.click(create);
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('pending-create')).toHaveLength(1),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ expect(
+ await screen.findByRole('combobox', { name: 'Semantic layer type' }),
+ ).toBeInTheDocument();
+ await act(async () => {
+ finishSave({ result: { uuid: 'old-layer' } });
+ await fetchMock.callHistory.flush(true);
+ });
+ expect(
+ screen.getByRole('combobox', { name: 'Semantic layer type' }),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('combobox', { name: 'Semantic layer' }),
+ ).not.toBeInTheDocument();
+ expect(fetchMock.callHistory.calls('setup-layers')).toHaveLength(2);
+ });
+
+ test('late layer creation refreshes the view picker after cancel without
stale UI', async () => {
+ const user = configure(true);
+ const addSuccessToast = jest.fn();
+ fetchMock.removeRoutes({ names: ['setup-types'] });
+ fetchMock.get('glob:*/api/v1/semantic_layer/types', {
+ result: [{ id: 'example', name: 'Example provider' }],
+ });
+ fetchMock.post('glob:*/api/v1/semantic_layer/schema/configuration', {
+ result: { type: 'object', properties: {} },
+ });
+ let finishSave!: (value: { result: { uuid: string } }) => void;
+ fetchMock.post(
+ 'glob:*/api/v1/semantic_layer/',
+ () =>
+ new Promise(resolve => {
+ finishSave = resolve;
+ }),
+ { name: 'pending-create' },
+ );
+ renderDatasetList(user, { addSuccessToast });
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ const typePicker = await screen.findByRole('combobox', {
+ name: 'Semantic layer type',
+ });
+ await waitFor(() => expect(typePicker).toBeEnabled());
+ fireEvent.mouseDown(typePicker);
+ await userEvent.click(
+ await screen.findByText('Example provider', {
+ selector: '.ant-select-item-option-content',
+ }),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Add' }));
+ const create = await screen.findByRole('button', { name: 'Create' });
+ await waitFor(() => expect(create).toBeEnabled());
+ await userEvent.click(create);
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('pending-create')).toHaveLength(1),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+ expect(
+ await screen.findByText(/No semantic layers are available to you/),
+ ).toBeInTheDocument();
+ expect(fetchMock.callHistory.calls('setup-layers')).toHaveLength(2);
+ fetchMock.removeRoutes({ names: ['setup-layers'] });
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/',
+ { result: [{ uuid: 'new-layer', name: 'Example provider' }] },
+ { name: 'refreshed-layers' },
+ );
+ await act(async () => {
+ finishSave({ result: { uuid: 'new-layer' } });
+ await fetchMock.callHistory.flush(true);
+ });
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('refreshed-layers')).toHaveLength(1),
+ );
+ const layerPicker = screen.getByRole('combobox', {
+ name: 'Semantic layer',
+ });
+ expect(layerPicker).toBeEnabled();
+ fireEvent.mouseDown(layerPicker);
+ expect(
+ await screen.findByText('Example provider', {
+ selector: '.ant-select-item-option-content',
+ }),
+ ).toBeInTheDocument();
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ expect(addSuccessToast).not.toHaveBeenCalled();
+ });
+
+ test('flag off keeps the setup entry unavailable', async () => {
+ const user = configure(true);
+ jest.mocked(isFeatureEnabled).mockReturnValue(false);
+ renderDatasetList(user);
+ expect(await screen.findByText('Datasets')).toBeInTheDocument();
+ expect(screen.queryByTestId('btn-create-new')).not.toBeInTheDocument();
Review Comment:
Good point. In `44f812c680` the test holds `_info` pending, waits until
write permission unlocks the ordinary dataset creation controls, and only then
checks that the semantic entry is absent. With the flag guard removed, the old
early assertion still passed under that delay, and the new one fails on the
unwanted entry.
##########
superset-frontend/src/pages/DatasetList/DatasetList.permissions.test.tsx:
##########
@@ -425,3 +432,301 @@ test('user with can_duplicate sees duplicate button only
for virtual datasets',
const virtualDuplicateButton = within(virtualRow!).getByTestId('copy');
expect(virtualDuplicateButton).toBeInTheDocument();
});
+
+describe('semantic layer setup transition', () => {
+ const configure = (canWriteLayer: boolean) => {
+ jest
+ .mocked(isFeatureEnabled)
+ .mockImplementation(flag => flag === 'SEMANTIC_LAYERS');
+ setupApiPermissions(['can_read', 'can_write']);
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/',
+ { result: [] },
+ { name: 'setup-layers' },
+ );
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/types',
+ { result: [] },
+ { name: 'setup-types' },
+ );
+ return {
+ ...mockAdminUser,
+ roles: {
+ Custom: [
+ ['can_read', 'SemanticLayer'],
+ ...(canWriteLayer ? [['can_write', 'SemanticLayer']] : []),
+ ],
+ },
+ };
+ };
+
+ const openView = async () => {
+ await userEvent.click(await screen.findByTestId('btn-create-new'));
+ await userEvent.click(
+ await screen.findByText('Semantic View', {
+ selector: '.ant-dropdown-menu-title-content',
+ }),
+ );
+ };
+
+ test('layer writers can switch dialogs and cancel back to a fresh layer
list', async () => {
+ renderDatasetList(configure(true));
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ expect(
+ await screen.findByText(
+ 'No semantic layer types are available. Ask your administrator to
check the semantic layer setup for this deployment.',
+ ),
+ ).toBeInTheDocument();
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ fireEvent.keyDown(screen.getByRole('dialog'), {
+ key: 'Escape',
+ keyCode: 27,
+ });
+ expect(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ ).toBeInTheDocument();
+ expect(screen.getAllByRole('dialog')).toHaveLength(1);
+ expect(fetchMock.callHistory.calls('setup-layers')).toHaveLength(2);
+ expect(
+ fetchMock.callHistory
+ .calls()
+ .filter(call => call.options.method === 'POST'),
+ ).toHaveLength(0);
+ await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
+ await waitFor(() =>
+ expect(screen.getByTestId('btn-create-new')).toHaveFocus(),
+ );
+ });
+
+ test('dataset writers without layer-write permission get administrator
guidance', async () => {
+ renderDatasetList(configure(false));
+ await openView();
+ expect(
+ await screen.findByText(
+ /Ask your administrator to set up a semantic layer or grant you
access/,
+ ),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'Create semantic layer' }),
+ ).not.toBeInTheDocument();
+ });
+
+ test.each([true, false])(
+ 'creation success=%s returns only after a successful save',
+ async succeeds => {
+ const user = configure(true);
+ fetchMock.removeRoutes({ names: ['setup-types'] });
+ fetchMock.get('glob:*/api/v1/semantic_layer/types', {
+ result: [{ id: 'example', name: 'Example provider' }],
+ });
+ fetchMock.post('glob:*/api/v1/semantic_layer/schema/configuration', {
+ result: { type: 'object', properties: {} },
+ });
+ fetchMock.post(
+ 'glob:*/api/v1/semantic_layer/',
+ succeeds ? { result: { uuid: 'new-layer' } } : 500,
+ { name: 'create-layer' },
+ );
+ renderDatasetList(user);
+ await openView();
+ await userEvent.click(
+ await screen.findByRole('button', { name: 'Create semantic layer' }),
+ );
+ const typePicker = await screen.findByRole('combobox', {
+ name: 'Semantic layer type',
+ });
+ await waitFor(() => expect(typePicker).toBeEnabled());
+ fireEvent.mouseDown(typePicker);
+ await userEvent.click(
+ await screen.findByText('Example provider', {
+ selector: '.ant-select-item-option-content',
+ }),
+ );
+ await userEvent.click(screen.getByRole('button', { name: 'Add' }));
+ const create = await screen.findByRole('button', { name: 'Create' });
+ await waitFor(() => expect(create).toBeEnabled());
+ fetchMock.removeRoutes({ names: ['setup-layers'] });
+ fetchMock.get(
+ 'glob:*/api/v1/semantic_layer/',
+ { result: [{ uuid: 'new-layer', name: 'Example provider' }] },
+ { name: 'refreshed-layers' },
+ );
+ await userEvent.click(create);
+ await waitFor(() =>
+ expect(fetchMock.callHistory.calls('create-layer')).toHaveLength(1),
+ );
+ if (succeeds) {
+ expect(
+ await screen.findByRole('combobox', { name: 'Semantic layer' }),
+ ).toHaveValue('');
Review Comment:
Tightened in `44f812c680`. After the refreshed list returns, the test checks
that the picker still shows its placeholder, that no item is selected, and that
no runtime-schema or views request has fired before an explicit selection.
Auto-selecting the refreshed layer passed the old empty-search-input check and
fails the new placeholder check.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]