mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-09 14:10:18 +00:00
* supported bulk edit for entity * added support for other database level entity and update the edit button and remove the add row button * added playwright test for databaseService,database,schema and Table * added the banner so the info around the import should be present * improve the route to protected, and move common button to utils * revert the admin only route change as user with editAll permission can perform the action * added check around the route, allow only for editAll access for the entity * change the route to the entityRoute file to organize in single place
This commit is contained in:
parent
99a2372fe4
commit
a313e0ec69
@ -0,0 +1,575 @@
|
||||
/*
|
||||
* Copyright 2025 Collate.
|
||||
* Licensed 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 { expect, test } from '@playwright/test';
|
||||
|
||||
import { SERVICE_TYPE } from '../../constant/service';
|
||||
import { GlobalSettingOptions } from '../../constant/settings';
|
||||
import { EntityDataClass } from '../../support/entity/EntityDataClass';
|
||||
import { TableClass } from '../../support/entity/TableClass';
|
||||
import {
|
||||
createNewPage,
|
||||
descriptionBoxReadOnly,
|
||||
getApiContext,
|
||||
redirectToHomePage,
|
||||
toastNotification,
|
||||
} from '../../utils/common';
|
||||
import {
|
||||
createColumnRowDetails,
|
||||
createCustomPropertiesForEntity,
|
||||
createDatabaseRowDetails,
|
||||
createDatabaseSchemaRowDetails,
|
||||
createTableRowDetails,
|
||||
fillDescriptionDetails,
|
||||
fillGlossaryTermDetails,
|
||||
fillRowDetails,
|
||||
fillTagDetails,
|
||||
pressKeyXTimes,
|
||||
validateImportStatus,
|
||||
} from '../../utils/importUtils';
|
||||
import { visitServiceDetailsPage } from '../../utils/service';
|
||||
|
||||
// use the admin user to login
|
||||
test.use({
|
||||
storageState: 'playwright/.auth/admin.json',
|
||||
});
|
||||
|
||||
const glossaryDetails = {
|
||||
name: EntityDataClass.glossaryTerm1.data.name,
|
||||
parent: EntityDataClass.glossary1.data.name,
|
||||
};
|
||||
|
||||
const databaseSchemaDetails1 = {
|
||||
...createDatabaseSchemaRowDetails(),
|
||||
glossary: glossaryDetails,
|
||||
};
|
||||
|
||||
const tableDetails1 = {
|
||||
...createTableRowDetails(),
|
||||
glossary: glossaryDetails,
|
||||
};
|
||||
|
||||
const columnDetails1 = {
|
||||
...createColumnRowDetails(),
|
||||
glossary: glossaryDetails,
|
||||
};
|
||||
|
||||
test.describe('Bulk Edit Entity', () => {
|
||||
test.beforeAll('setup pre-test', async ({ browser }, testInfo) => {
|
||||
const { apiContext, afterAction } = await createNewPage(browser);
|
||||
|
||||
testInfo.setTimeout(90000);
|
||||
await EntityDataClass.preRequisitesForTests(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test.afterAll('Cleanup', async ({ browser }, testInfo) => {
|
||||
const { apiContext, afterAction } = await createNewPage(browser);
|
||||
|
||||
testInfo.setTimeout(90000);
|
||||
await EntityDataClass.postRequisitesForTests(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await redirectToHomePage(page);
|
||||
});
|
||||
|
||||
test('Database service', async ({ page }) => {
|
||||
test.slow(true);
|
||||
|
||||
const table = new TableClass();
|
||||
let customPropertyRecord: Record<string, string> = {};
|
||||
|
||||
const { apiContext, afterAction } = await getApiContext(page);
|
||||
await table.create(apiContext);
|
||||
|
||||
await test.step('create custom properties for extension edit', async () => {
|
||||
customPropertyRecord = await createCustomPropertiesForEntity(
|
||||
page,
|
||||
GlobalSettingOptions.DATABASES
|
||||
);
|
||||
});
|
||||
|
||||
await test.step('Perform bulk edit action', async () => {
|
||||
const databaseDetails = {
|
||||
...createDatabaseRowDetails(),
|
||||
domains: EntityDataClass.domain1.responseData,
|
||||
glossary: glossaryDetails,
|
||||
};
|
||||
|
||||
await visitServiceDetailsPage(
|
||||
page,
|
||||
{
|
||||
name: table.service.name,
|
||||
type: SERVICE_TYPE.Database,
|
||||
},
|
||||
false
|
||||
);
|
||||
await page.click('[data-testid="bulk-edit-table"]');
|
||||
|
||||
// Adding manual wait for the file to load
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.waitForSelector('[data-testid="loader"]', {
|
||||
state: 'detached',
|
||||
});
|
||||
|
||||
// Adding some assertion to make sure that CSV loaded correctly
|
||||
await expect(
|
||||
page.locator('.InovuaReactDataGrid__header-layout')
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Next' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Previous' })
|
||||
).not.toBeVisible();
|
||||
|
||||
// Click on first cell and edit
|
||||
|
||||
await page.click(
|
||||
'.InovuaReactDataGrid__row--first > .InovuaReactDataGrid__row-cell-wrap > .InovuaReactDataGrid__cell--first'
|
||||
);
|
||||
await fillRowDetails(
|
||||
{
|
||||
...databaseDetails,
|
||||
name: table.database.name,
|
||||
owners: [
|
||||
EntityDataClass.user1.responseData?.['displayName'],
|
||||
EntityDataClass.user2.responseData?.['displayName'],
|
||||
],
|
||||
},
|
||||
page,
|
||||
customPropertyRecord
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
|
||||
await validateImportStatus(page, {
|
||||
passed: '2',
|
||||
processed: '2',
|
||||
failed: '0',
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
await page
|
||||
.locator('.inovua-react-toolkit-load-mask__background-layer')
|
||||
.waitFor({ state: 'detached' });
|
||||
|
||||
await toastNotification(page, /details updated successfully/);
|
||||
|
||||
// Verify Details updated
|
||||
await expect(page.getByTestId('column-name')).toHaveText(
|
||||
`${table.database.name}${databaseDetails.displayName}`
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.locator(`.ant-table-cell ${descriptionBoxReadOnly}`)
|
||||
).toContainText('Playwright Database description.');
|
||||
|
||||
// Verify Owners
|
||||
await expect(
|
||||
page.locator(`.ant-table-cell [data-testid="owner-label"]`)
|
||||
).toContainText(EntityDataClass.user1.responseData?.['displayName']);
|
||||
await expect(
|
||||
page.locator(`.ant-table-cell [data-testid="owner-label"]`)
|
||||
).toContainText(EntityDataClass.user2.responseData?.['displayName']);
|
||||
|
||||
// Verify Tags
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: 'Sensitive',
|
||||
})
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: 'Tier1',
|
||||
})
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: EntityDataClass.glossaryTerm1.data.displayName,
|
||||
})
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await table.delete(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test('Database', async ({ page }) => {
|
||||
test.slow(true);
|
||||
|
||||
let customPropertyRecord: Record<string, string> = {};
|
||||
|
||||
const table = new TableClass();
|
||||
|
||||
const { apiContext, afterAction } = await getApiContext(page);
|
||||
await table.create(apiContext);
|
||||
|
||||
await test.step('create custom properties for extension edit', async () => {
|
||||
customPropertyRecord = await createCustomPropertiesForEntity(
|
||||
page,
|
||||
GlobalSettingOptions.DATABASE_SCHEMA
|
||||
);
|
||||
});
|
||||
|
||||
await test.step('Perform bulk edit action', async () => {
|
||||
// visit entity Page
|
||||
await visitServiceDetailsPage(
|
||||
page,
|
||||
{
|
||||
name: table.service.name,
|
||||
type: SERVICE_TYPE.Database,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const databaseResponse = page.waitForResponse(
|
||||
`/api/v1/databases/name/*${table.database.name}?**`
|
||||
);
|
||||
await page.getByTestId(table.database.name).click();
|
||||
await databaseResponse;
|
||||
|
||||
await page.click('[data-testid="bulk-edit-table"]');
|
||||
|
||||
// Adding manual wait for the file to load
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.waitForSelector('[data-testid="loader"]', {
|
||||
state: 'detached',
|
||||
});
|
||||
|
||||
// Adding some assertion to make sure that CSV loaded correctly
|
||||
await expect(
|
||||
page.locator('.InovuaReactDataGrid__header-layout')
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Next' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Previous' })
|
||||
).not.toBeVisible();
|
||||
|
||||
// click on last row first cell
|
||||
await page.click(
|
||||
'.InovuaReactDataGrid__row--first > .InovuaReactDataGrid__row-cell-wrap > .InovuaReactDataGrid__cell--first'
|
||||
);
|
||||
|
||||
// Click on first cell and edit
|
||||
await fillRowDetails(
|
||||
{
|
||||
...databaseSchemaDetails1,
|
||||
name: table.schema.name,
|
||||
owners: [
|
||||
EntityDataClass.user1.responseData?.['displayName'],
|
||||
EntityDataClass.user2.responseData?.['displayName'],
|
||||
],
|
||||
domains: EntityDataClass.domain1.responseData,
|
||||
},
|
||||
page,
|
||||
customPropertyRecord
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
const loader = page.locator(
|
||||
'.inovua-react-toolkit-load-mask__background-layer'
|
||||
);
|
||||
|
||||
await loader.waitFor({ state: 'hidden' });
|
||||
|
||||
await validateImportStatus(page, {
|
||||
passed: '2',
|
||||
processed: '2',
|
||||
failed: '0',
|
||||
});
|
||||
|
||||
await page.waitForSelector('.InovuaReactDataGrid__header-layout', {
|
||||
state: 'visible',
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
await page
|
||||
.locator('.inovua-react-toolkit-load-mask__background-layer')
|
||||
.waitFor({ state: 'detached' });
|
||||
|
||||
await toastNotification(page, /details updated successfully/);
|
||||
|
||||
// Verify Details updated
|
||||
await expect(page.getByTestId('column-name')).toHaveText(
|
||||
`${table.schema.name}${databaseSchemaDetails1.displayName}`
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.locator(`.ant-table-cell ${descriptionBoxReadOnly}`)
|
||||
).toContainText('Playwright Database Schema description.');
|
||||
|
||||
// Verify Owners
|
||||
await expect(
|
||||
page.locator(`.ant-table-cell [data-testid="owner-label"]`)
|
||||
).toContainText(EntityDataClass.user1.responseData?.['displayName']);
|
||||
await expect(
|
||||
page.locator(`.ant-table-cell [data-testid="owner-label"]`)
|
||||
).toContainText(EntityDataClass.user2.responseData?.['displayName']);
|
||||
|
||||
await page.getByTestId('column-display-name').click();
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('loader', { state: 'hidden' });
|
||||
|
||||
// Verify Tags
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: 'Sensitive',
|
||||
})
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: 'Tier1',
|
||||
})
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: EntityDataClass.glossaryTerm1.data.displayName,
|
||||
})
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await table.delete(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test('Database Schema', async ({ page }) => {
|
||||
test.slow(true);
|
||||
|
||||
let customPropertyRecord: Record<string, string> = {};
|
||||
const table = new TableClass();
|
||||
|
||||
const { apiContext, afterAction } = await getApiContext(page);
|
||||
await table.create(apiContext);
|
||||
|
||||
await test.step('create custom properties for extension edit', async () => {
|
||||
customPropertyRecord = await createCustomPropertiesForEntity(
|
||||
page,
|
||||
GlobalSettingOptions.TABLES
|
||||
);
|
||||
});
|
||||
|
||||
await test.step('Perform bulk edit action', async () => {
|
||||
// visit entity page
|
||||
await visitServiceDetailsPage(
|
||||
page,
|
||||
{
|
||||
name: table.service.name,
|
||||
type: SERVICE_TYPE.Database,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const databaseResponse = page.waitForResponse(
|
||||
`/api/v1/databases/name/*${table.database.name}?**`
|
||||
);
|
||||
await page.getByTestId(table.database.name).click();
|
||||
await databaseResponse;
|
||||
const databaseSchemaResponse = page.waitForResponse(
|
||||
`/api/v1/databaseSchemas/name/*${table.schema.name}?*`
|
||||
);
|
||||
await page.getByTestId(table.schema.name).click();
|
||||
await databaseSchemaResponse;
|
||||
|
||||
await page.click('[data-testid="bulk-edit-table"]');
|
||||
|
||||
// Adding manual wait for the file to load
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Adding some assertion to make sure that CSV loaded correctly
|
||||
await expect(
|
||||
page.locator('.InovuaReactDataGrid__header-layout')
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Next' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Previous' })
|
||||
).not.toBeVisible();
|
||||
|
||||
// Click on first cell and edit
|
||||
await page.click(
|
||||
'.InovuaReactDataGrid__row--first > .InovuaReactDataGrid__row-cell-wrap > .InovuaReactDataGrid__cell--first'
|
||||
);
|
||||
await fillRowDetails(
|
||||
{
|
||||
...tableDetails1,
|
||||
name: table.entity.name,
|
||||
owners: [
|
||||
EntityDataClass.user1.responseData?.['displayName'],
|
||||
EntityDataClass.user2.responseData?.['displayName'],
|
||||
],
|
||||
domains: EntityDataClass.domain1.responseData,
|
||||
},
|
||||
page,
|
||||
customPropertyRecord
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
|
||||
await validateImportStatus(page, {
|
||||
passed: '2',
|
||||
processed: '2',
|
||||
failed: '0',
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
await toastNotification(page, /details updated successfully/);
|
||||
|
||||
// Verify Details updated
|
||||
await expect(page.getByTestId('column-name')).toHaveText(
|
||||
`${table.entity.name}${tableDetails1.displayName}`
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.locator(`.ant-table-cell ${descriptionBoxReadOnly}`)
|
||||
).toContainText('Playwright Table description');
|
||||
|
||||
// Go to Table Page
|
||||
await page
|
||||
.getByTestId('column-display-name')
|
||||
.getByTestId(table.entity.name)
|
||||
.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForSelector('loader', { state: 'hidden' });
|
||||
|
||||
// Verify Domain
|
||||
await expect(page.getByTestId('domain-link')).toContainText(
|
||||
EntityDataClass.domain1.responseData.displayName
|
||||
);
|
||||
|
||||
// Verify Owners
|
||||
await expect(page.getByTestId('owner-label')).toContainText(
|
||||
EntityDataClass.user1.responseData?.['displayName']
|
||||
);
|
||||
await expect(page.getByTestId('owner-label')).toContainText(
|
||||
EntityDataClass.user2.responseData?.['displayName']
|
||||
);
|
||||
|
||||
// Verify Tags
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: 'Sensitive',
|
||||
})
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: 'Tier1',
|
||||
})
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: EntityDataClass.glossaryTerm1.data.displayName,
|
||||
})
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await table.delete(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test('Table', async ({ page }) => {
|
||||
test.slow();
|
||||
|
||||
const tableEntity = new TableClass();
|
||||
|
||||
const { apiContext, afterAction } = await getApiContext(page);
|
||||
await tableEntity.create(apiContext);
|
||||
|
||||
await test.step('Perform bulk edit action', async () => {
|
||||
await tableEntity.visitEntityPage(page);
|
||||
|
||||
await page.click('[data-testid="bulk-edit-table"]');
|
||||
|
||||
// Adding manual wait for the file to load
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Adding some assertion to make sure that CSV loaded correctly
|
||||
await expect(
|
||||
page.locator('.InovuaReactDataGrid__header-layout')
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Next' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Previous' })
|
||||
).not.toBeVisible();
|
||||
|
||||
// click on row first cell
|
||||
await page.click(
|
||||
'.InovuaReactDataGrid__row--first > .InovuaReactDataGrid__row-cell-wrap > .InovuaReactDataGrid__cell--first'
|
||||
);
|
||||
|
||||
await page.click('.InovuaReactDataGrid__cell--cell-active');
|
||||
|
||||
await pressKeyXTimes(page, 2, 'ArrowRight');
|
||||
|
||||
await fillDescriptionDetails(page, columnDetails1.description);
|
||||
|
||||
await pressKeyXTimes(page, 5, 'ArrowRight');
|
||||
|
||||
await fillTagDetails(page, columnDetails1.tag);
|
||||
|
||||
await page
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowRight', { delay: 100 });
|
||||
await fillGlossaryTermDetails(page, columnDetails1.glossary);
|
||||
|
||||
// Reverse traves to first cell to fill the details
|
||||
await page.click('.InovuaReactDataGrid__cell--cell-active');
|
||||
await page
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await page.click('[type="button"] >> text="Next"', { force: true });
|
||||
|
||||
await validateImportStatus(page, {
|
||||
passed: '7',
|
||||
processed: '7',
|
||||
failed: '0',
|
||||
});
|
||||
|
||||
await page.click('[type="button"] >> text="Update"', { force: true });
|
||||
await page
|
||||
.locator('.inovua-react-toolkit-load-mask__background-layer')
|
||||
.waitFor({ state: 'detached' });
|
||||
|
||||
await toastNotification(page, /details updated successfully/);
|
||||
|
||||
// Verify Details updated
|
||||
await expect(
|
||||
page.getByRole('cell', { name: 'Playwright Table column' })
|
||||
).toBeVisible();
|
||||
|
||||
// Verify Tags
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: 'Sensitive',
|
||||
})
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', {
|
||||
name: EntityDataClass.glossaryTerm1.data.displayName,
|
||||
})
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await tableEntity.delete(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
});
|
||||
@ -23,7 +23,6 @@ import { Operation } from '../../generated/entity/policies/policy';
|
||||
import AddCustomMetricPage from '../../pages/AddCustomMetricPage/AddCustomMetricPage';
|
||||
import { CustomizablePage } from '../../pages/CustomizablePage/CustomizablePage';
|
||||
import DataQualityPage from '../../pages/DataQuality/DataQualityPage';
|
||||
import BulkEntityImportPage from '../../pages/EntityImport/BulkEntityImportPage/BulkEntityImportPage';
|
||||
import ForbiddenPage from '../../pages/ForbiddenPage/ForbiddenPage';
|
||||
import TagPage from '../../pages/TagPage/TagPage';
|
||||
import { checkPermission, userPermissions } from '../../utils/PermissionsUtils';
|
||||
@ -282,13 +281,6 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||
<Switch>
|
||||
<Route exact component={ForbiddenPage} path={ROUTES.FORBIDDEN} />
|
||||
|
||||
{/* Handle Entity Import pages */}
|
||||
<Route
|
||||
exact
|
||||
component={BulkEntityImportPage}
|
||||
path={ROUTES.ENTITY_IMPORT}
|
||||
/>
|
||||
|
||||
<Route exact component={MyDataPage} path={ROUTES.MY_DATA} />
|
||||
<Route exact component={TourPageComponent} path={ROUTES.TOUR} />
|
||||
<Route exact component={ExplorePageV1} path={ROUTES.EXPLORE} />
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 Collate.
|
||||
* Licensed 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, { useCallback, useEffect, useState } from 'react';
|
||||
import { Redirect, Route, Switch, useHistory, useParams } from 'react-router';
|
||||
import { SUPPORTED_BULK_IMPORT_EDIT_ENTITY } from '../../constants/BulkImport.constant';
|
||||
import { ROUTES } from '../../constants/constants';
|
||||
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
|
||||
import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvider.interface';
|
||||
import { useFqn } from '../../hooks/useFqn';
|
||||
import BulkEntityImportPage from '../../pages/EntityImport/BulkEntityImportPage/BulkEntityImportPage';
|
||||
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
|
||||
|
||||
const EntityImportRouter = () => {
|
||||
const history = useHistory();
|
||||
const { fqn } = useFqn();
|
||||
const { entityType } = useParams<{ entityType: ResourceEntity }>();
|
||||
const { getEntityPermissionByFqn } = usePermissionProvider();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [entityPermission, setEntityPermission] = useState(
|
||||
DEFAULT_ENTITY_PERMISSION
|
||||
);
|
||||
|
||||
const fetchResourcePermission = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const entityPermission = await getEntityPermissionByFqn(entityType, fqn);
|
||||
setEntityPermission(entityPermission);
|
||||
} catch (error) {
|
||||
// will not show logs
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [entityType, fqn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fqn && SUPPORTED_BULK_IMPORT_EDIT_ENTITY.includes(entityType)) {
|
||||
fetchResourcePermission();
|
||||
} else {
|
||||
history.push(ROUTES.NOT_FOUND);
|
||||
}
|
||||
}, [fqn, entityType]);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
{entityPermission.EditAll && (
|
||||
<Route
|
||||
exact
|
||||
component={BulkEntityImportPage}
|
||||
path={[ROUTES.ENTITY_IMPORT, ROUTES.BULK_EDIT_ENTITY_WITH_FQN]}
|
||||
/>
|
||||
)}
|
||||
<Redirect to={ROUTES.NOT_FOUND} />
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntityImportRouter;
|
||||
@ -16,6 +16,7 @@ import { ROUTES } from '../../constants/constants';
|
||||
import { EntityType } from '../../enums/entity.enum';
|
||||
import EntityVersionPage from '../../pages/EntityVersionPage/EntityVersionPage.component';
|
||||
import entityUtilClassBase from '../../utils/EntityUtilClassBase';
|
||||
import EntityImportRouter from './EntityImportRouter';
|
||||
|
||||
const EntityRouter = () => {
|
||||
const { entityType } = useParams<{ entityType: EntityType }>();
|
||||
@ -27,6 +28,12 @@ const EntityRouter = () => {
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
{/* Handle Entity Import and Edit pages */}
|
||||
<Route
|
||||
component={EntityImportRouter}
|
||||
path={[ROUTES.ENTITY_IMPORT, ROUTES.BULK_EDIT_ENTITY_WITH_FQN]}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
component={EntityVersionPage}
|
||||
|
||||
@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright 2025 Collate.
|
||||
* Licensed 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 ReactDataGrid from '@inovua/reactdatagrid-community';
|
||||
import { Button, Col, Row } from 'antd';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { readString } from 'react-papaparse';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ENTITY_BULK_EDIT_STEPS } from '../../constants/BulkEdit.constants';
|
||||
import { EntityType } from '../../enums/entity.enum';
|
||||
import { useFqn } from '../../hooks/useFqn';
|
||||
import {
|
||||
getBulkEditCSVExportEntityApi,
|
||||
getBulkEntityEditBreadcrumbList,
|
||||
} from '../../utils/EntityBulkEdit/EntityBulkEditUtils';
|
||||
import Banner from '../common/Banner/Banner';
|
||||
import { ImportStatus } from '../common/EntityImport/ImportStatus/ImportStatus.component';
|
||||
import Loader from '../common/Loader/Loader';
|
||||
import TitleBreadcrumb from '../common/TitleBreadcrumb/TitleBreadcrumb.component';
|
||||
import { TitleBreadcrumbProps } from '../common/TitleBreadcrumb/TitleBreadcrumb.interface';
|
||||
import { useEntityExportModalProvider } from '../Entity/EntityExportModalProvider/EntityExportModalProvider.component';
|
||||
import Stepper from '../Settings/Services/Ingestion/IngestionStepper/IngestionStepper.component';
|
||||
import { BulkEditEntityProps } from './BulkEditEntity.interface';
|
||||
|
||||
const BulkEditEntity = ({
|
||||
onKeyDown,
|
||||
onEditStop,
|
||||
onEditStart,
|
||||
onEditComplete,
|
||||
dataSource,
|
||||
columns,
|
||||
setGridRef,
|
||||
activeStep,
|
||||
handleBack,
|
||||
handleValidate,
|
||||
isValidating,
|
||||
validationData,
|
||||
validateCSVData,
|
||||
activeAsyncImportJob,
|
||||
onCSVReadComplete,
|
||||
}: BulkEditEntityProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { fqn } = useFqn();
|
||||
const { entityType } = useParams<{ entityType: EntityType }>();
|
||||
const { triggerExportForBulkEdit, csvExportData, clearCSVExportData } =
|
||||
useEntityExportModalProvider();
|
||||
|
||||
const breadcrumbList: TitleBreadcrumbProps['titleLinks'] = useMemo(
|
||||
() => getBulkEntityEditBreadcrumbList(entityType, fqn),
|
||||
[entityType, fqn]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
triggerExportForBulkEdit({
|
||||
name: fqn,
|
||||
onExport: getBulkEditCSVExportEntityApi(entityType),
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (csvExportData) {
|
||||
readString(csvExportData, {
|
||||
worker: true,
|
||||
skipEmptyLines: true,
|
||||
complete: onCSVReadComplete,
|
||||
});
|
||||
}
|
||||
}, [csvExportData]);
|
||||
|
||||
useEffect(() => {
|
||||
// clear the csvExportData data from the state
|
||||
return () => {
|
||||
clearCSVExportData();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col span={24}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumbList} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Stepper
|
||||
activeStep={activeStep}
|
||||
className="w-max-600 mx-auto"
|
||||
steps={ENTITY_BULK_EDIT_STEPS}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
{activeAsyncImportJob?.jobId && (
|
||||
<Banner
|
||||
className="border-radius"
|
||||
isLoading={!activeAsyncImportJob.error}
|
||||
message={
|
||||
activeAsyncImportJob.error ?? activeAsyncImportJob.message ?? ''
|
||||
}
|
||||
type={activeAsyncImportJob.error ? 'error' : 'success'}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{isEmpty(csvExportData) ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<>
|
||||
<Col span={24}>
|
||||
{activeStep === 1 && (
|
||||
<ReactDataGrid
|
||||
editable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
defaultActiveCell={[0, 0]}
|
||||
handle={setGridRef}
|
||||
idProperty="id"
|
||||
loading={isValidating}
|
||||
minRowHeight={30}
|
||||
showZebraRows={false}
|
||||
style={{ height: 'calc(100vh - 245px)' }}
|
||||
onEditComplete={onEditComplete}
|
||||
onEditStart={onEditStart}
|
||||
onEditStop={onEditStop}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeStep === 2 && validationData && (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<ImportStatus csvImportResult={validationData} />
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
{validateCSVData && (
|
||||
<ReactDataGrid
|
||||
idProperty="id"
|
||||
loading={isValidating}
|
||||
style={{ height: 'calc(100vh - 300px)' }}
|
||||
{...validateCSVData}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
{activeStep > 0 && (
|
||||
<Col span={24}>
|
||||
<div className="float-right import-footer">
|
||||
{activeStep > 1 && (
|
||||
<Button disabled={isValidating} onClick={handleBack}>
|
||||
{t('label.previous')}
|
||||
</Button>
|
||||
)}
|
||||
{activeStep < 3 && (
|
||||
<Button
|
||||
className="m-l-sm"
|
||||
disabled={isValidating}
|
||||
type="primary"
|
||||
onClick={handleValidate}>
|
||||
{activeStep === 2 ? t('label.update') : t('label.next')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkEditEntity;
|
||||
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 Collate.
|
||||
* Licensed 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 {
|
||||
TypeColumn,
|
||||
TypeComputedProps,
|
||||
} from '@inovua/reactdatagrid-community/types';
|
||||
import { VALIDATION_STEP } from '../../constants/BulkImport.constant';
|
||||
import { CSVImportResult } from '../../generated/type/csvImportResult';
|
||||
import { CSVImportJobType } from '../BulkImport/BulkEntityImport.interface';
|
||||
|
||||
export interface BulkEditEntityProps {
|
||||
dataSource: Record<string, string>[];
|
||||
columns: TypeColumn[];
|
||||
activeStep: VALIDATION_STEP;
|
||||
activeAsyncImportJob?: CSVImportJobType;
|
||||
isValidating: boolean;
|
||||
validationData?: CSVImportResult;
|
||||
validateCSVData?: {
|
||||
columns: TypeColumn[];
|
||||
dataSource: Record<string, string>[];
|
||||
};
|
||||
handleBack: () => void;
|
||||
handleValidate: () => Promise<void>;
|
||||
setGridRef: React.Dispatch<
|
||||
React.SetStateAction<React.MutableRefObject<TypeComputedProps | null>>
|
||||
>;
|
||||
onKeyDown: (event: KeyboardEvent) => void;
|
||||
onEditStop: () => void;
|
||||
onEditStart: () => void;
|
||||
onCSVReadComplete: (results: { data: string[][] }) => void;
|
||||
onEditComplete: ({ value, columnId, rowId }: any) => void;
|
||||
}
|
||||
@ -47,8 +47,9 @@ import {
|
||||
patchDatabaseSchemaDetails,
|
||||
} from '../../../../rest/databaseAPI';
|
||||
import { searchQuery } from '../../../../rest/searchAPI';
|
||||
import { getBulkEditButton } from '../../../../utils/EntityBulkEdit/EntityBulkEditUtils';
|
||||
import {
|
||||
getEntityName,
|
||||
getEntityBulkEditPath,
|
||||
highlightSearchText,
|
||||
} from '../../../../utils/EntityUtils';
|
||||
import { stringToHTML } from '../../../../utils/StringsUtils';
|
||||
@ -58,6 +59,7 @@ import DisplayName from '../../../common/DisplayName/DisplayName';
|
||||
import ErrorPlaceHolder from '../../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import NextPrevious from '../../../common/NextPrevious/NextPrevious';
|
||||
import { PagingHandlerParams } from '../../../common/NextPrevious/NextPrevious.interface';
|
||||
import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component';
|
||||
import RichTextEditorPreviewerV1 from '../../../common/RichTextEditor/RichTextEditorPreviewerV1';
|
||||
import Searchbar from '../../../common/SearchBarComponent/SearchBar.component';
|
||||
import Table from '../../../common/Table/Table';
|
||||
@ -271,7 +273,7 @@ export const DatabaseSchemaTable = ({
|
||||
width: 120,
|
||||
render: (owners: EntityReference[]) =>
|
||||
!isEmpty(owners) && owners.length > 0 ? (
|
||||
owners.map((owner: EntityReference) => getEntityName(owner))
|
||||
<OwnerLabel owners={owners} />
|
||||
) : (
|
||||
<Typography.Text data-testid="no-owner-text">
|
||||
{NO_DATA_PLACEHOLDER}
|
||||
@ -290,6 +292,12 @@ export const DatabaseSchemaTable = ({
|
||||
[handleDisplayNameUpdate, allowEditDisplayNamePermission]
|
||||
);
|
||||
|
||||
const handleEditTable = () => {
|
||||
history.push({
|
||||
pathname: getEntityBulkEditPath(EntityType.DATABASE, decodedDatabaseFQN),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDatabaseSchema();
|
||||
}, [decodedDatabaseFQN, pageSize, showDeletedSchemas, isDatabaseDeleted]);
|
||||
@ -324,6 +332,10 @@ export const DatabaseSchemaTable = ({
|
||||
data-testid="database-databaseSchemas"
|
||||
dataSource={schemas}
|
||||
defaultVisibleColumns={DEFAULT_DATABASE_SCHEMA_VISIBLE_COLUMNS}
|
||||
extraTableFilters={getBulkEditButton(
|
||||
permissions.databaseSchema.EditAll,
|
||||
handleEditTable
|
||||
)}
|
||||
loading={isLoading}
|
||||
locale={{
|
||||
emptyText: <ErrorPlaceHolder className="m-y-md" />,
|
||||
|
||||
@ -27,6 +27,7 @@ import {
|
||||
import { EntityTags, TagFilterOptions } from 'Models';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ReactComponent as IconEdit } from '../../../assets/svg/edit-new.svg';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
||||
import {
|
||||
@ -54,8 +55,10 @@ import { TagLabel } from '../../../generated/type/tagLabel';
|
||||
import { useFqn } from '../../../hooks/useFqn';
|
||||
import { getTestCaseExecutionSummary } from '../../../rest/testAPI';
|
||||
import { getPartialNameFromTableFQN } from '../../../utils/CommonUtils';
|
||||
import { getBulkEditButton } from '../../../utils/EntityBulkEdit/EntityBulkEditUtils';
|
||||
import {
|
||||
getColumnSorter,
|
||||
getEntityBulkEditPath,
|
||||
getEntityName,
|
||||
getFrequentlyJoinedColumns,
|
||||
highlightSearchArrayElement,
|
||||
@ -98,6 +101,7 @@ import {
|
||||
|
||||
const SchemaTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const [testCaseSummary, setTestCaseSummary] = useState<TestSummary>();
|
||||
const [searchedColumns, setSearchedColumns] = useState<Column[]>([]);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
@ -573,6 +577,12 @@ const SchemaTable = () => {
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
const handleEditTable = () => {
|
||||
history.push({
|
||||
pathname: getEntityBulkEditPath(EntityType.TABLE, decodedEntityFqn),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedRowKeys(nestedTableFqnKeys);
|
||||
}, [searchText]);
|
||||
@ -596,6 +606,15 @@ const SchemaTable = () => {
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Searchbar
|
||||
removeMargin
|
||||
placeholder={t('message.find-in-table')}
|
||||
searchValue={searchText}
|
||||
typingInterval={500}
|
||||
onSearch={handleSearchAction}
|
||||
/>
|
||||
</Col>
|
||||
<Col id="schemaDetails" span={24}>
|
||||
<Table
|
||||
bordered
|
||||
@ -605,16 +624,10 @@ const SchemaTable = () => {
|
||||
dataSource={data}
|
||||
defaultVisibleColumns={DEFAULT_SCHEMA_TABLE_VISIBLE_COLUMNS}
|
||||
expandable={expandableConfig}
|
||||
extraTableFilters={
|
||||
<Searchbar
|
||||
removeMargin
|
||||
placeholder={t('message.find-in-table')}
|
||||
searchValue={searchText}
|
||||
typingInterval={500}
|
||||
onSearch={handleSearchAction}
|
||||
/>
|
||||
}
|
||||
extraTableFiltersClassName="justify-between"
|
||||
extraTableFilters={getBulkEditButton(
|
||||
tablePermissions.EditAll,
|
||||
handleEditTable
|
||||
)}
|
||||
locale={{
|
||||
emptyText: <FilterTablePlaceHolder />,
|
||||
}}
|
||||
|
||||
@ -14,9 +14,18 @@ import { Form, Input, Modal } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import { isString } from 'lodash';
|
||||
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { getCurrentISODate } from '../../../utils/date-time/DateTimeUtils';
|
||||
import { isBulkEditRoute } from '../../../utils/EntityBulkEdit/EntityBulkEditUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import Banner from '../../common/Banner/Banner';
|
||||
import {
|
||||
@ -38,6 +47,8 @@ export const EntityExportModalProvider = ({
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const [exportData, setExportData] = useState<ExportData | null>(null);
|
||||
const [downloading, setDownloading] = useState<boolean>(false);
|
||||
|
||||
@ -45,6 +56,13 @@ export const EntityExportModalProvider = ({
|
||||
|
||||
const [csvExportJob, setCSVExportJob] = useState<Partial<CSVExportJob>>();
|
||||
|
||||
const [csvExportData, setCSVExportData] = useState<string>();
|
||||
|
||||
const isBulkEdit = useMemo(
|
||||
() => isBulkEditRoute(location.pathname),
|
||||
[location]
|
||||
);
|
||||
|
||||
const handleCancel = () => {
|
||||
setExportData(null);
|
||||
};
|
||||
@ -106,65 +124,82 @@ export const EntityExportModalProvider = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCSVExportSuccess = (data: string, fileName?: string) => {
|
||||
handleDownload(
|
||||
data,
|
||||
fileName ?? `${exportData?.name}_${getCurrentISODate()}`
|
||||
);
|
||||
setDownloading(false);
|
||||
handleCancel();
|
||||
setCSVExportJob(undefined);
|
||||
csvExportJobRef.current = undefined;
|
||||
};
|
||||
|
||||
const handleCSVExportJobUpdate = (
|
||||
response: Partial<CSVExportWebsocketResponse>
|
||||
) => {
|
||||
// If multiple tab is open, then we need to check if the tab has active job or not before initiating the download
|
||||
if (!csvExportJobRef.current) {
|
||||
return;
|
||||
}
|
||||
const updatedCSVExportJob: Partial<CSVExportJob> = {
|
||||
...response,
|
||||
...csvExportJobRef.current,
|
||||
};
|
||||
|
||||
setCSVExportJob(updatedCSVExportJob);
|
||||
|
||||
csvExportJobRef.current = updatedCSVExportJob;
|
||||
|
||||
if (response.status === 'COMPLETED' && response.data) {
|
||||
handleCSVExportSuccess(
|
||||
response.data ?? '',
|
||||
csvExportJobRef.current?.fileName
|
||||
);
|
||||
} else {
|
||||
const handleCSVExportSuccess = useCallback(
|
||||
(data: string, fileName?: string) => {
|
||||
if (isBulkEdit) {
|
||||
setCSVExportData(data);
|
||||
} else {
|
||||
handleDownload(
|
||||
data,
|
||||
fileName ?? `${exportData?.name}_${getCurrentISODate()}`
|
||||
);
|
||||
}
|
||||
setDownloading(false);
|
||||
}
|
||||
};
|
||||
handleCancel();
|
||||
setCSVExportJob(undefined);
|
||||
csvExportJobRef.current = undefined;
|
||||
},
|
||||
[isBulkEdit]
|
||||
);
|
||||
|
||||
const handleCSVExportJobUpdate = useCallback(
|
||||
(response: Partial<CSVExportWebsocketResponse>) => {
|
||||
// If multiple tab is open, then we need to check if the tab has active job or not before initiating the download
|
||||
if (!csvExportJobRef.current) {
|
||||
return;
|
||||
}
|
||||
const updatedCSVExportJob: Partial<CSVExportJob> = {
|
||||
...response,
|
||||
...csvExportJobRef.current,
|
||||
};
|
||||
|
||||
setCSVExportJob(updatedCSVExportJob);
|
||||
|
||||
csvExportJobRef.current = updatedCSVExportJob;
|
||||
|
||||
if (response.status === 'COMPLETED' && response.data) {
|
||||
handleCSVExportSuccess(
|
||||
response.data ?? '',
|
||||
csvExportJobRef.current?.fileName
|
||||
);
|
||||
} else {
|
||||
setDownloading(false);
|
||||
}
|
||||
},
|
||||
[isBulkEdit, handleCSVExportSuccess]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (exportData) {
|
||||
form.setFieldValue(
|
||||
'fileName',
|
||||
`${exportData.name}_${getCurrentISODate()}`
|
||||
);
|
||||
if (isBulkEdit) {
|
||||
handleExport({ fileName: 'bulk-edit' });
|
||||
} else {
|
||||
form.setFieldValue(
|
||||
'fileName',
|
||||
`${exportData.name}_${getCurrentISODate()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [exportData]);
|
||||
}, [isBulkEdit, exportData]);
|
||||
|
||||
const providerValue = useMemo(
|
||||
() => ({
|
||||
csvExportData,
|
||||
clearCSVExportData: () => setCSVExportData(undefined),
|
||||
showModal,
|
||||
triggerExportForBulkEdit: (exportData: ExportData) => {
|
||||
setExportData(exportData);
|
||||
},
|
||||
onUpdateCSVExportJob: handleCSVExportJobUpdate,
|
||||
}),
|
||||
[]
|
||||
[isBulkEdit, csvExportData, handleCSVExportJobUpdate]
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityExportModalContext.Provider value={providerValue}>
|
||||
<>
|
||||
{children}
|
||||
{exportData && (
|
||||
{exportData && !isBulkEdit && (
|
||||
<Modal
|
||||
centered
|
||||
open
|
||||
|
||||
@ -33,6 +33,9 @@ export type ExportData = {
|
||||
onExport: (name: string) => Promise<CSVExportResponse | string>;
|
||||
};
|
||||
export interface EntityExportModalContextProps {
|
||||
csvExportData?: string;
|
||||
clearCSVExportData: () => void;
|
||||
showModal: (data: ExportData) => void;
|
||||
triggerExportForBulkEdit: (data: ExportData) => void;
|
||||
onUpdateCSVExportJob: (data: Partial<CSVExportWebsocketResponse>) => void;
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
*/
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
EntityExportModalProvider,
|
||||
useEntityExportModalProvider,
|
||||
@ -28,6 +29,12 @@ const mockShowModal: ExportData = {
|
||||
onExport: jest.fn().mockImplementation(() => Promise.resolve(mockExportJob)),
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useLocation: jest.fn().mockImplementation(() => ({
|
||||
pathname: '/mock-path',
|
||||
})),
|
||||
}));
|
||||
|
||||
const ConsumerComponent = () => {
|
||||
const { showModal } = useEntityExportModalProvider();
|
||||
|
||||
@ -140,4 +147,21 @@ describe('EntityExportModalProvider component', () => {
|
||||
|
||||
expect(await screen.findByText(mockExportJob.message)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Export modal should not be visible if route is bulk edit', async () => {
|
||||
(useLocation as jest.Mock).mockReturnValue({
|
||||
pathname: '/bulk/edit',
|
||||
});
|
||||
render(
|
||||
<EntityExportModalProvider>
|
||||
<ConsumerComponent />
|
||||
</EntityExportModalProvider>
|
||||
);
|
||||
|
||||
const manageBtn = await screen.findByText('Manage');
|
||||
|
||||
fireEvent.click(manageBtn);
|
||||
|
||||
expect(screen.queryByTestId('export-entity-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@ -414,7 +414,7 @@ const NavBar = ({
|
||||
socket && socket.off(SOCKET_EVENTS.CSV_EXPORT_CHANNEL);
|
||||
socket && socket.off(SOCKET_EVENTS.BACKGROUND_JOB_CHANNEL);
|
||||
};
|
||||
}, [socket]);
|
||||
}, [socket, onUpdateCSVExportJob]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOMVersion();
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2025 Collate.
|
||||
* Licensed 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 i18n from '../utils/i18next/LocalUtil';
|
||||
import { VALIDATION_STEP } from './BulkImport.constant';
|
||||
|
||||
export const ENTITY_BULK_EDIT_STEPS = [
|
||||
{
|
||||
name: i18n.t('label.preview-and-edit'),
|
||||
step: VALIDATION_STEP.EDIT_VALIDATE,
|
||||
},
|
||||
{
|
||||
name: i18n.t('label.update'),
|
||||
step: VALIDATION_STEP.UPDATE,
|
||||
},
|
||||
];
|
||||
@ -11,8 +11,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { startCase } from 'lodash';
|
||||
import { ResourceEntity } from '../context/PermissionProvider/PermissionProvider.interface';
|
||||
import i18n from '../utils/i18next/LocalUtil';
|
||||
|
||||
export const SUPPORTED_BULK_IMPORT_EDIT_ENTITY = [
|
||||
ResourceEntity.TABLE,
|
||||
ResourceEntity.DATABASE_SERVICE,
|
||||
ResourceEntity.DATABASE,
|
||||
ResourceEntity.DATABASE_SCHEMA,
|
||||
];
|
||||
|
||||
export enum VALIDATION_STEP {
|
||||
UPLOAD = 0,
|
||||
EDIT_VALIDATE = 1,
|
||||
|
||||
@ -287,6 +287,10 @@ export const ROUTES = {
|
||||
|
||||
// Entity Import
|
||||
ENTITY_IMPORT: `/bulk/import/${PLACEHOLDER_ROUTE_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_FQN}`,
|
||||
|
||||
// Entity Bulk Edit
|
||||
BULK_EDIT_ENTITY: `/bulk/edit`,
|
||||
BULK_EDIT_ENTITY_WITH_FQN: `/bulk/edit/${PLACEHOLDER_ROUTE_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_FQN}`,
|
||||
};
|
||||
|
||||
export const SOCKET_EVENTS = {
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "Durchsuchen",
|
||||
"browse-app-plural": "Browse Apps",
|
||||
"browse-csv-file": "CSV-Datei durchsuchen",
|
||||
"bulk-edit": "Massenbearbeitung",
|
||||
"by-entity": "Nach {{entity}}",
|
||||
"by-lowercase": "von",
|
||||
"ca-certs": "CA-Zertifikate",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "Browse",
|
||||
"browse-app-plural": "Browse Apps",
|
||||
"browse-csv-file": "Browse CSV file",
|
||||
"bulk-edit": "Bulk Edit",
|
||||
"by-entity": "By {{entity}}",
|
||||
"by-lowercase": "by",
|
||||
"ca-certs": "CA Certs",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "Explorar",
|
||||
"browse-app-plural": "Explorar Apps",
|
||||
"browse-csv-file": "Examinar archivo CSV",
|
||||
"bulk-edit": "Edición masiva",
|
||||
"by-entity": "Por {{entity}}",
|
||||
"by-lowercase": "por",
|
||||
"ca-certs": "Certificados CA",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "Naviguer",
|
||||
"browse-app-plural": "Browse Apps",
|
||||
"browse-csv-file": "Naviguer le fichier CSV",
|
||||
"bulk-edit": "Modification en masse",
|
||||
"by-entity": "Par {{entity}}",
|
||||
"by-lowercase": "par",
|
||||
"ca-certs": "Certificats CA",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "Explorar",
|
||||
"browse-app-plural": "Explorar aplicacións",
|
||||
"browse-csv-file": "Explorar ficheiro CSV",
|
||||
"bulk-edit": "Edición masiva",
|
||||
"by-entity": "Por {{entity}}",
|
||||
"by-lowercase": "por",
|
||||
"ca-certs": "Certificados CA",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "עיון",
|
||||
"browse-app-plural": "עיין באפליקציות",
|
||||
"browse-csv-file": "עיין בקובץ CSV",
|
||||
"bulk-edit": "עריכה מרובה",
|
||||
"by-entity": "לפי {{entity}}",
|
||||
"by-lowercase": "לפי",
|
||||
"ca-certs": "תעודות CA",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "Browse",
|
||||
"browse-app-plural": "Browse Apps",
|
||||
"browse-csv-file": "CSVファイルを見る",
|
||||
"bulk-edit": "一括編集",
|
||||
"by-entity": "By {{entity}}",
|
||||
"by-lowercase": "by",
|
||||
"ca-certs": "CA Certs",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "찾아보기",
|
||||
"browse-app-plural": "앱 찾아보기",
|
||||
"browse-csv-file": "CSV 파일 찾아보기",
|
||||
"bulk-edit": "일괄 편집",
|
||||
"by-entity": "{{entity}} 기준",
|
||||
"by-lowercase": "의",
|
||||
"ca-certs": "CA 인증서",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "ब्राउझ करा",
|
||||
"browse-app-plural": "ॲप्स ब्राउझ करा",
|
||||
"browse-csv-file": "CSV फाइल ब्राउझ करा",
|
||||
"bulk-edit": "सामूहिक संपादन",
|
||||
"by-entity": "{{entity}} द्वारे",
|
||||
"by-lowercase": "द्वारे",
|
||||
"ca-certs": "CA प्रमाणपत्रे",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "Bladeren",
|
||||
"browse-app-plural": "Blader door apps",
|
||||
"browse-csv-file": "Blader door CSV-bestand",
|
||||
"bulk-edit": "Bulk bewerken",
|
||||
"by-entity": "Door {{entity}}",
|
||||
"by-lowercase": "door",
|
||||
"ca-certs": "CA-certificaten",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "مرور",
|
||||
"browse-app-plural": "مرور برنامهها",
|
||||
"browse-csv-file": "مرور فایل CSV",
|
||||
"bulk-edit": "ویرایش دستهای",
|
||||
"by-entity": "توسط {{entity}}",
|
||||
"by-lowercase": "توسط",
|
||||
"ca-certs": "گواهیهای CA",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "Navegar",
|
||||
"browse-app-plural": "Navegar em Apps",
|
||||
"browse-csv-file": "Navegar no arquivo CSV",
|
||||
"bulk-edit": "Edição em massa",
|
||||
"by-entity": "Por {{entity}}",
|
||||
"by-lowercase": "por",
|
||||
"ca-certs": "Certificados CA",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "Navegar",
|
||||
"browse-app-plural": "Navegar em Apps",
|
||||
"browse-csv-file": "Navegar no arquivo CSV",
|
||||
"bulk-edit": "Edição em massa",
|
||||
"by-entity": "Por {{entity}}",
|
||||
"by-lowercase": "por",
|
||||
"ca-certs": "Certificados CA",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "Просмотр",
|
||||
"browse-app-plural": "Browse Apps",
|
||||
"browse-csv-file": "Просмотр CSV-файла",
|
||||
"bulk-edit": "Массовое редактирование",
|
||||
"by-entity": "{{entity}}",
|
||||
"by-lowercase": "к",
|
||||
"ca-certs": "Сертификаты CA",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "เรียกดู",
|
||||
"browse-app-plural": "เรียกดูแอปพลิเคชัน",
|
||||
"browse-csv-file": "เรียกดูไฟล์ CSV",
|
||||
"bulk-edit": "แก้ไขเป็นกลุ่ม",
|
||||
"by-entity": "โดย {{entity}}",
|
||||
"by-lowercase": "โดย",
|
||||
"ca-certs": "ใบรับรอง CA",
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
"browse": "浏览",
|
||||
"browse-app-plural": "浏览应用",
|
||||
"browse-csv-file": "打开 CSV 文件",
|
||||
"bulk-edit": "批量编辑",
|
||||
"by-entity": "按{{entity}}",
|
||||
"by-lowercase": "by",
|
||||
"ca-certs": "CA 证书",
|
||||
|
||||
@ -18,6 +18,7 @@ import { compare } from 'fast-json-patch';
|
||||
import { isUndefined } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import DisplayName from '../../components/common/DisplayName/DisplayName';
|
||||
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import NextPrevious from '../../components/common/NextPrevious/NextPrevious';
|
||||
@ -45,7 +46,9 @@ import {
|
||||
patchTableDetails,
|
||||
TableListParams,
|
||||
} from '../../rest/tableAPI';
|
||||
import { getBulkEditButton } from '../../utils/EntityBulkEdit/EntityBulkEditUtils';
|
||||
import entityUtilClassBase from '../../utils/EntityUtilClassBase';
|
||||
import { getEntityBulkEditPath } from '../../utils/EntityUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
|
||||
interface SchemaTablesTabProps {
|
||||
@ -56,6 +59,7 @@ function SchemaTablesTab({
|
||||
isVersionView = false,
|
||||
}: Readonly<SchemaTablesTabProps>) {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const [tableData, setTableData] = useState<Array<Table>>([]);
|
||||
const [tableDataLoading, setTableDataLoading] = useState<boolean>(true);
|
||||
const { permissions } = usePermissionProvider();
|
||||
@ -201,6 +205,15 @@ function SchemaTablesTab({
|
||||
[handleDisplayNameUpdate, allowEditDisplayNamePermission]
|
||||
);
|
||||
|
||||
const handleEditTable = () => {
|
||||
history.push({
|
||||
pathname: getEntityBulkEditPath(
|
||||
EntityType.DATABASE_SCHEMA,
|
||||
decodedDatabaseSchemaFQN
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (viewDatabaseSchemaPermission && decodedDatabaseSchemaFQN) {
|
||||
if (pagingCursor?.cursorData?.cursorType) {
|
||||
@ -228,29 +241,30 @@ function SchemaTablesTab({
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
{!isVersionView && (
|
||||
<Col span={24}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Switch
|
||||
checked={tableFilters.showDeletedTables}
|
||||
data-testid="show-deleted"
|
||||
onClick={handleShowDeletedTables}
|
||||
/>
|
||||
<Typography.Text className="m-l-xs">
|
||||
{t('label.deleted')}
|
||||
</Typography.Text>{' '}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
<Col span={24}>
|
||||
<TableAntd
|
||||
bordered
|
||||
columns={tableColumn}
|
||||
data-testid="databaseSchema-tables"
|
||||
dataSource={tableData}
|
||||
extraTableFilters={
|
||||
!isVersionView && (
|
||||
<>
|
||||
<span>
|
||||
<Switch
|
||||
checked={tableFilters.showDeletedTables}
|
||||
data-testid="show-deleted"
|
||||
onClick={handleShowDeletedTables}
|
||||
/>
|
||||
<Typography.Text className="m-l-xs">
|
||||
{t('label.deleted')}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
|
||||
{getBulkEditButton(permissions.table.EditAll, handleEditTable)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
loading={tableDataLoading}
|
||||
locale={{
|
||||
emptyText: (
|
||||
|
||||
@ -28,7 +28,8 @@ import React, {
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePapaParse } from 'react-papaparse';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import BulkEditEntity from '../../../components/BulkEditEntity/BulkEditEntity.component';
|
||||
import {
|
||||
CSVImportAsyncWebsocketResponse,
|
||||
CSVImportJobType,
|
||||
@ -57,6 +58,7 @@ import {
|
||||
getCSVStringFromColumnsAndDataSource,
|
||||
getEntityColumnsAndDataSourceFromCSV,
|
||||
} from '../../../utils/CSV/CSV.utils';
|
||||
import { isBulkEditRoute } from '../../../utils/EntityBulkEdit/EntityBulkEditUtils';
|
||||
import {
|
||||
getBulkEntityImportBreadcrumbList,
|
||||
getImportedEntityType,
|
||||
@ -79,6 +81,7 @@ const BulkEntityImportPage = () => {
|
||||
);
|
||||
const activeStepRef = useRef<VALIDATION_STEP>(VALIDATION_STEP.UPLOAD);
|
||||
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const { entityType } = useParams<{ entityType: EntityType }>();
|
||||
const { fqn } = useFqn();
|
||||
@ -94,6 +97,11 @@ const BulkEntityImportPage = () => {
|
||||
MutableRefObject<TypeComputedProps | null>
|
||||
>({ current: null });
|
||||
|
||||
const isBulkEdit = useMemo(
|
||||
() => isBulkEditRoute(location.pathname),
|
||||
[location]
|
||||
);
|
||||
|
||||
const importedEntityType = useMemo(
|
||||
() => getImportedEntityType(entityType),
|
||||
[entityType]
|
||||
@ -455,118 +463,147 @@ const BulkEntityImportPage = () => {
|
||||
entity: entityType,
|
||||
})}>
|
||||
<Row className="p-x-lg" gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumbList} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Stepper activeStep={activeStep} steps={ENTITY_IMPORT_STEPS} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{activeAsyncImportJob?.jobId && (
|
||||
<Banner
|
||||
className="border-radius"
|
||||
isLoading={!activeAsyncImportJob.error}
|
||||
message={
|
||||
activeAsyncImportJob.error ?? activeAsyncImportJob.message ?? ''
|
||||
}
|
||||
type={activeAsyncImportJob.error ? 'error' : 'success'}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{activeStep === 0 && (
|
||||
<>
|
||||
{validationData?.abortReason ? (
|
||||
<Card className="m-t-lg">
|
||||
<Space
|
||||
align="center"
|
||||
className="w-full justify-center p-lg text-center"
|
||||
direction="vertical"
|
||||
size={16}>
|
||||
<Typography.Text
|
||||
className="text-center"
|
||||
data-testid="abort-reason">
|
||||
<strong className="d-block">{t('label.aborted')}</strong>{' '}
|
||||
{validationData.abortReason}
|
||||
</Typography.Text>
|
||||
<Space size={16}>
|
||||
<Button
|
||||
ghost
|
||||
data-testid="cancel-button"
|
||||
type="primary"
|
||||
onClick={handleRetryCsvUpload}>
|
||||
{t('label.back')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
) : (
|
||||
<UploadFile fileType=".csv" onCSVUploaded={handleLoadData} />
|
||||
{isBulkEdit ? (
|
||||
<BulkEditEntity
|
||||
activeAsyncImportJob={activeAsyncImportJob}
|
||||
activeStep={activeStep}
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
handleBack={handleBack}
|
||||
handleValidate={handleValidate}
|
||||
isValidating={isValidating}
|
||||
setGridRef={setGridRef}
|
||||
validateCSVData={validateCSVData}
|
||||
validationData={validationData}
|
||||
onCSVReadComplete={onCSVReadComplete}
|
||||
onEditComplete={onEditComplete}
|
||||
onEditStart={onEditStart}
|
||||
onEditStop={onEditStop}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Col span={24}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumbList} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Stepper activeStep={activeStep} steps={ENTITY_IMPORT_STEPS} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{activeAsyncImportJob?.jobId && (
|
||||
<Banner
|
||||
className="border-radius"
|
||||
isLoading={!activeAsyncImportJob.error}
|
||||
message={
|
||||
activeAsyncImportJob.error ??
|
||||
activeAsyncImportJob.message ??
|
||||
''
|
||||
}
|
||||
type={activeAsyncImportJob.error ? 'error' : 'success'}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{activeStep === 1 && (
|
||||
<ReactDataGrid
|
||||
editable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
defaultActiveCell={[0, 0]}
|
||||
handle={setGridRef}
|
||||
idProperty="id"
|
||||
loading={isValidating}
|
||||
minRowHeight={30}
|
||||
showZebraRows={false}
|
||||
style={{ height: 'calc(100vh - 245px)' }}
|
||||
onEditComplete={onEditComplete}
|
||||
onEditStart={onEditStart}
|
||||
onEditStop={onEditStop}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
)}
|
||||
{activeStep === 2 && validationData && (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<ImportStatus csvImportResult={validationData} />
|
||||
</Col>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{activeStep === 0 && (
|
||||
<>
|
||||
{validationData?.abortReason ? (
|
||||
<Card className="m-t-lg">
|
||||
<Space
|
||||
align="center"
|
||||
className="w-full justify-center p-lg text-center"
|
||||
direction="vertical"
|
||||
size={16}>
|
||||
<Typography.Text
|
||||
className="text-center"
|
||||
data-testid="abort-reason">
|
||||
<strong className="d-block">
|
||||
{t('label.aborted')}
|
||||
</strong>{' '}
|
||||
{validationData.abortReason}
|
||||
</Typography.Text>
|
||||
<Space size={16}>
|
||||
<Button
|
||||
ghost
|
||||
data-testid="cancel-button"
|
||||
type="primary"
|
||||
onClick={handleRetryCsvUpload}>
|
||||
{t('label.back')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
) : (
|
||||
<UploadFile
|
||||
fileType=".csv"
|
||||
onCSVUploaded={handleLoadData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{activeStep === 1 && (
|
||||
<ReactDataGrid
|
||||
editable
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
defaultActiveCell={[0, 0]}
|
||||
handle={setGridRef}
|
||||
idProperty="id"
|
||||
loading={isValidating}
|
||||
minRowHeight={30}
|
||||
showZebraRows={false}
|
||||
style={{ height: 'calc(100vh - 245px)' }}
|
||||
onEditComplete={onEditComplete}
|
||||
onEditStart={onEditStart}
|
||||
onEditStop={onEditStop}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
)}
|
||||
{activeStep === 2 && validationData && (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<ImportStatus csvImportResult={validationData} />
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
{validateCSVData && (
|
||||
<ReactDataGrid
|
||||
idProperty="id"
|
||||
loading={isValidating}
|
||||
style={{ height: 'calc(100vh - 300px)' }}
|
||||
{...validateCSVData}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{activeStep > 0 && (
|
||||
<Col span={24}>
|
||||
{validateCSVData && (
|
||||
<ReactDataGrid
|
||||
idProperty="id"
|
||||
loading={isValidating}
|
||||
style={{ height: 'calc(100vh - 300px)' }}
|
||||
{...validateCSVData}
|
||||
/>
|
||||
{activeStep === 1 && (
|
||||
<Button data-testid="add-row-btn" onClick={handleAddRow}>
|
||||
{`+ ${t('label.add-row')}`}
|
||||
</Button>
|
||||
)}
|
||||
<div className="float-right import-footer">
|
||||
{activeStep > 0 && (
|
||||
<Button disabled={isValidating} onClick={handleBack}>
|
||||
{t('label.previous')}
|
||||
</Button>
|
||||
)}
|
||||
{activeStep < 3 && (
|
||||
<Button
|
||||
className="m-l-sm"
|
||||
disabled={isValidating}
|
||||
type="primary"
|
||||
onClick={handleValidate}>
|
||||
{activeStep === 2 ? t('label.update') : t('label.next')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{activeStep > 0 && (
|
||||
<Col span={24}>
|
||||
{activeStep === 1 && (
|
||||
<Button data-testid="add-row-btn" onClick={handleAddRow}>
|
||||
{`+ ${t('label.add-row')}`}
|
||||
</Button>
|
||||
)}
|
||||
<div className="float-right import-footer">
|
||||
{activeStep > 0 && (
|
||||
<Button disabled={isValidating} onClick={handleBack}>
|
||||
{t('label.previous')}
|
||||
</Button>
|
||||
)}
|
||||
{activeStep < 3 && (
|
||||
<Button
|
||||
className="m-l-sm"
|
||||
disabled={isValidating}
|
||||
type="primary"
|
||||
onClick={handleValidate}>
|
||||
{activeStep === 2 ? t('label.update') : t('label.next')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
</PageLayoutV1>
|
||||
|
||||
@ -19,7 +19,7 @@ import { isUndefined } from 'lodash';
|
||||
import { EntityTags, ServiceTypes } from 'Models';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1';
|
||||
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import Loader from '../../components/common/Loader/Loader';
|
||||
@ -41,6 +41,8 @@ import { EntityType } from '../../enums/entity.enum';
|
||||
import { Paging } from '../../generated/type/paging';
|
||||
import { UsePagingInterface } from '../../hooks/paging/usePaging';
|
||||
import { ServicesType } from '../../interface/service.interface';
|
||||
import { getBulkEditButton } from '../../utils/EntityBulkEdit/EntityBulkEditUtils';
|
||||
import { getEntityBulkEditPath } from '../../utils/EntityUtils';
|
||||
import {
|
||||
callServicePatchAPI,
|
||||
getServiceMainTabColumns,
|
||||
@ -89,6 +91,7 @@ function ServiceMainTabContent({
|
||||
serviceCategory: ServiceTypes;
|
||||
}>();
|
||||
const { permissions } = usePermissionProvider();
|
||||
const history = useHistory();
|
||||
const [pageData, setPageData] = useState<ServicePageData[]>([]);
|
||||
|
||||
const tier = getTierTags(serviceDetails?.tags ?? []);
|
||||
@ -200,6 +203,15 @@ function ServiceMainTabContent({
|
||||
[serviceCategory]
|
||||
);
|
||||
|
||||
const handleEditTable = () => {
|
||||
history.push({
|
||||
pathname: getEntityBulkEditPath(
|
||||
EntityType.DATABASE_SERVICE,
|
||||
serviceDetails.fullyQualifiedName ?? ''
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
editTagsPermission,
|
||||
editGlossaryTermsPermission,
|
||||
@ -260,16 +272,24 @@ function ServiceMainTabContent({
|
||||
DEFAULT_SERVICE_TAB_VISIBLE_COLUMNS
|
||||
}
|
||||
extraTableFilters={
|
||||
<span>
|
||||
<Switch
|
||||
checked={showDeleted}
|
||||
data-testid="show-deleted"
|
||||
onClick={onShowDeletedChange}
|
||||
/>
|
||||
<Typography.Text className="m-l-xs">
|
||||
{t('label.deleted')}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
<>
|
||||
<span>
|
||||
<Switch
|
||||
checked={showDeleted}
|
||||
data-testid="show-deleted"
|
||||
onClick={onShowDeletedChange}
|
||||
/>
|
||||
<Typography.Text className="m-l-xs">
|
||||
{t('label.deleted')}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
|
||||
{entityType === EntityType.DATABASE_SERVICE &&
|
||||
getBulkEditButton(
|
||||
servicePermission.EditAll,
|
||||
handleEditTable
|
||||
)}
|
||||
</>
|
||||
}
|
||||
locale={{
|
||||
emptyText: <ErrorPlaceHolder className="m-y-md" />,
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2025 Collate.
|
||||
* Licensed 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 Icon from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
import { ReactComponent as IconEdit } from '../../assets/svg/edit-new.svg';
|
||||
import { TitleBreadcrumbProps } from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.interface';
|
||||
import { ROUTES } from '../../constants/constants';
|
||||
import { EntityType } from '../../enums/entity.enum';
|
||||
import {
|
||||
exportDatabaseDetailsInCSV,
|
||||
exportDatabaseSchemaDetailsInCSV,
|
||||
} from '../../rest/databaseAPI';
|
||||
import { exportDatabaseServiceDetailsInCSV } from '../../rest/serviceAPI';
|
||||
import { exportTableDetailsInCSV } from '../../rest/tableAPI';
|
||||
import entityUtilClassBase from '../EntityUtilClassBase';
|
||||
import Fqn from '../Fqn';
|
||||
import i18n from '../i18next/LocalUtil';
|
||||
|
||||
export const isBulkEditRoute = (pathname: string) => {
|
||||
return pathname.includes(ROUTES.BULK_EDIT_ENTITY);
|
||||
};
|
||||
|
||||
export const getBulkEntityEditBreadcrumbList = (
|
||||
entityType: EntityType,
|
||||
fqn: string
|
||||
): TitleBreadcrumbProps['titleLinks'] => [
|
||||
{
|
||||
name: Fqn.split(fqn).pop(),
|
||||
url: entityUtilClassBase.getEntityLink(entityType, fqn),
|
||||
},
|
||||
{
|
||||
name: i18n.t('label.bulk-edit'),
|
||||
url: '',
|
||||
activeTitle: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const getBulkEditCSVExportEntityApi = (entityType: EntityType) => {
|
||||
switch (entityType) {
|
||||
case EntityType.DATABASE_SERVICE:
|
||||
return exportDatabaseServiceDetailsInCSV;
|
||||
|
||||
case EntityType.DATABASE:
|
||||
return exportDatabaseDetailsInCSV;
|
||||
|
||||
case EntityType.DATABASE_SCHEMA:
|
||||
return exportDatabaseSchemaDetailsInCSV;
|
||||
|
||||
case EntityType.TABLE:
|
||||
return exportTableDetailsInCSV;
|
||||
|
||||
default:
|
||||
return exportTableDetailsInCSV;
|
||||
}
|
||||
};
|
||||
|
||||
export const getBulkEditButton = (
|
||||
hasPermission: boolean,
|
||||
onClickHandler: () => void
|
||||
) => {
|
||||
return hasPermission ? (
|
||||
<Button
|
||||
className="text-primary p-0"
|
||||
data-testid="bulk-edit-table"
|
||||
icon={<Icon component={IconEdit} />}
|
||||
type="text"
|
||||
onClick={onClickHandler}>
|
||||
{i18n.t('label.edit')}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
@ -2493,6 +2493,13 @@ export const getEntityImportPath = (entityType: EntityType, fqn: string) => {
|
||||
).replace(PLACEHOLDER_ROUTE_FQN, fqn);
|
||||
};
|
||||
|
||||
export const getEntityBulkEditPath = (entityType: EntityType, fqn: string) => {
|
||||
return ROUTES.BULK_EDIT_ENTITY_WITH_FQN.replace(
|
||||
PLACEHOLDER_ROUTE_ENTITY_TYPE,
|
||||
entityType
|
||||
).replace(PLACEHOLDER_ROUTE_FQN, fqn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the node type based on whether it's a source or target node
|
||||
* @param node - The node to update
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user