This is an automated email from the ASF dual-hosted git repository.
jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 70494b5e1a [#9807][#9976][#9978] web(UI): support details drawer
componet for tag/policy/job template basic info and associated info (#9931)
70494b5e1a is described below
commit 70494b5e1a82c513205fedbeafea253a573dff57
Author: Qian Xia <[email protected]>
AuthorDate: Thu Feb 26 14:01:37 2026 +0800
[#9807][#9976][#9978] web(UI): support details drawer componet for
tag/policy/job template basic info and associated info (#9931)
### What changes were proposed in this pull request?
<img width="2890" height="1862" alt="image"
src="https://github.com/user-attachments/assets/8d3ee1fb-6da6-4cf4-b435-5f394c0d7b19"
/>
<img width="2896" height="1862" alt="image"
src="https://github.com/user-attachments/assets/31f3ab59-b916-465b-bb19-2c339929a8d3"
/>
<img width="2896" height="1878" alt="image"
src="https://github.com/user-attachments/assets/56f5289e-d1e1-47b7-a452-9fe34b522a5a"
/>
<img width="3168" height="1862" alt="image"
src="https://github.com/user-attachments/assets/9cd315e5-44ed-4d4e-9f17-2b38e6af60c3"
/>
### Why are the changes needed?
N/A
Fix: #9807 , #9976, #9978
### Does this PR introduce _any_ user-facing change?
N/A
### How was this patch tested?
manually
---------
Co-authored-by: Jerry Shao <[email protected]>
---
.github/workflows/frontend-integration-test.yml | 2 +-
.../test/web/ui/CatalogsPageDorisTest.java | 2 +-
.../test/web/ui/pages/CatalogsPage.java | 37 ++
web-v2/web/src/app/catalogs/page.js | 50 ++-
.../catalogs/rightContent/CreateCatalogDialog.js | 4 +-
.../catalogs/rightContent/CreateSchemaDialog.js | 20 +-
.../app/catalogs/rightContent/CreateTableDialog.js | 1 +
.../entitiesContent/CatalogDetailsPage.js | 3 +-
.../entitiesContent/FilesetDetailsPage.js | 4 +-
.../entitiesContent/ModelDetailsPage.js | 108 +++---
.../entitiesContent/SchemaDetailsPage.js | 10 +-
.../entitiesContent/TableDetailsPage.js | 4 +-
.../entitiesContent/TopicDetailsPage.js | 2 +-
web-v2/web/src/app/compliance/policies/page.js | 191 +++++-----
web-v2/web/src/app/compliance/tags/page.js | 61 +++-
web-v2/web/src/app/jobTemplates/page.js | 394 ++++++++++++---------
web-v2/web/src/app/jobs/page.js | 53 ++-
.../web/src/components/EntityPropertiesFormItem.js | 294 +++++++--------
18 files changed, 708 insertions(+), 532 deletions(-)
diff --git a/.github/workflows/frontend-integration-test.yml
b/.github/workflows/frontend-integration-test.yml
index a1e7ce5125..d9f4232bc1 100644
--- a/.github/workflows/frontend-integration-test.yml
+++ b/.github/workflows/frontend-integration-test.yml
@@ -37,7 +37,7 @@ jobs:
- scripts/**
- server/**
- server-common/**
- - web/**
+ - web-v2/**
- build.gradle.kts
- gradle.properties
- gradlew
diff --git
a/web-v2/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/CatalogsPageDorisTest.java
b/web-v2/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/CatalogsPageDorisTest.java
index 1bea908d2c..2fe2f937be 100644
---
a/web-v2/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/CatalogsPageDorisTest.java
+++
b/web-v2/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/CatalogsPageDorisTest.java
@@ -262,7 +262,7 @@ public class CatalogsPageDorisTest extends BaseWebIT {
@Test
@Order(7)
public void testBackHomePage() throws InterruptedException {
- clickAndWait(catalogsPage.backHomeBtn);
+ catalogsPage.clickBackHomeBtn();
Assertions.assertTrue(catalogsPage.verifyBackHomePage());
}
}
diff --git
a/web-v2/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/pages/CatalogsPage.java
b/web-v2/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/pages/CatalogsPage.java
index e1ac9c5c3a..5ea601cfd6 100644
---
a/web-v2/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/pages/CatalogsPage.java
+++
b/web-v2/integration-test/src/test/java/org/apache/gravitino/integration/test/web/ui/pages/CatalogsPage.java
@@ -598,6 +598,43 @@ public class CatalogsPage extends BaseWebIT {
}
}
+ public void clickBackHomeBtn() {
+ try {
+ String urlBefore = driver.getCurrentUrl();
+ WebDriverWait shortWait = new WebDriverWait(driver,
Duration.ofSeconds(10));
+ WebDriverWait wait = new WebDriverWait(driver,
Duration.ofSeconds(MAX_TIMEOUT));
+
+ // Strategy 1: Normal click
+ wait.until(ExpectedConditions.elementToBeClickable(backHomeBtn));
+ clickAndWait(backHomeBtn);
+ if (isUrlChanged(urlBefore, shortWait)) {
+ LOG.info("Back home navigation succeeded via normal click");
+ return;
+ }
+ LOG.warn("Normal back home click did not navigate, trying JavaScript
click...");
+
+ // Strategy 2: JavaScript click
+ ((JavascriptExecutor) driver).executeScript("arguments[0].click();",
backHomeBtn);
+ Thread.sleep(ACTION_SLEEP * 1000);
+ if (isUrlChanged(urlBefore, shortWait)) {
+ LOG.info("Back home navigation succeeded via JavaScript click");
+ return;
+ }
+ LOG.warn("JavaScript back home click did not navigate, trying direct URL
navigation...");
+
+ // Strategy 3: Extract href and navigate directly
+ String href = backHomeBtn.getAttribute("href");
+ if (href != null && !href.isEmpty()) {
+ driver.get(href);
+ LOG.info("Back home navigation succeeded via direct URL: {}", href);
+ return;
+ }
+ LOG.error("Failed to navigate back home: href was empty");
+ } catch (Exception e) {
+ LOG.error(e.getMessage(), e);
+ }
+ }
+
private boolean isElementPresent(String xpath, WebDriverWait wait) {
try {
wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath(xpath)));
diff --git a/web-v2/web/src/app/catalogs/page.js
b/web-v2/web/src/app/catalogs/page.js
index 2356e9a469..2231c9d6eb 100644
--- a/web-v2/web/src/app/catalogs/page.js
+++ b/web-v2/web/src/app/catalogs/page.js
@@ -87,7 +87,6 @@ const CatalogsListPage = () => {
async function fetchDependsData() {
if ([...searchParams.keys()].length) {
const { metalake, catalog, catalogType, schema, table, fileset, topic,
model, version } = routeParams
- await dispatch(setActivatedDetailsLoading(true))
if (metalake) {
dispatch(fetchTags({ metalake, details: true }))
@@ -103,7 +102,9 @@ const CatalogsListPage = () => {
if (!store.catalogs.length) {
await dispatch(fetchCatalogs({ metalake }))
}
- dispatch(getCatalogDetails({ init: true, metalake, catalog }))
+ await dispatch(setActivatedDetailsLoading(true))
+ await dispatch(getCatalogDetails({ init: true, metalake, catalog }))
+ await dispatch(setActivatedDetailsLoading(false))
await dispatch(fetchSchemas({ init: true, page: 'catalogs',
metalake, catalog, catalogType }))
dispatch(
getCurrentEntityTags({
@@ -147,7 +148,9 @@ const CatalogsListPage = () => {
default:
break
}
- dispatch(getSchemaDetails({ init: true, metalake, catalog, schema }))
+ await dispatch(setActivatedDetailsLoading(true))
+ await dispatch(getSchemaDetails({ init: true, metalake, catalog,
schema }))
+ await dispatch(setActivatedDetailsLoading(false))
dispatch(
getCurrentEntityTags({
init: true,
@@ -175,10 +178,13 @@ const CatalogsListPage = () => {
}
switch (catalogType) {
case 'relational':
- await dispatch(fetchTables({ init: true, page: 'schemas',
metalake, catalog, schema }))
+ store.tables.length === 0 &&
+ (await dispatch(fetchTables({ init: true, page: 'schemas',
metalake, catalog, schema })))
await dispatch(resetActivatedDetails())
+ await dispatch(setActivatedDetailsLoading(true))
await dispatch(getTableDetails({ init: true, metalake, catalog,
schema, table }))
- await dispatch(
+ await dispatch(setActivatedDetailsLoading(false))
+ dispatch(
getCurrentEntityTags({
init: true,
metalake,
@@ -186,7 +192,7 @@ const CatalogsListPage = () => {
metadataObjectFullName: `${catalog}.${schema}.${table}`
})
)
- await dispatch(
+ dispatch(
getCurrentEntityPolicies({
init: true,
metalake,
@@ -197,9 +203,12 @@ const CatalogsListPage = () => {
)
break
case 'fileset':
- await dispatch(fetchFilesets({ init: true, page: 'schemas',
metalake, catalog, schema }))
+ store.filesets.length === 0 &&
+ (await dispatch(fetchFilesets({ init: true, page: 'schemas',
metalake, catalog, schema })))
+ await dispatch(setActivatedDetailsLoading(true))
await dispatch(getFilesetDetails({ init: true, metalake,
catalog, schema, fileset }))
- await dispatch(
+ await dispatch(setActivatedDetailsLoading(false))
+ dispatch(
getCurrentEntityTags({
init: true,
metalake,
@@ -208,7 +217,7 @@ const CatalogsListPage = () => {
details: true
})
)
- await dispatch(
+ dispatch(
getCurrentEntityPolicies({
init: true,
metalake,
@@ -219,9 +228,12 @@ const CatalogsListPage = () => {
)
break
case 'messaging':
- await dispatch(fetchTopics({ init: true, page: 'schemas',
metalake, catalog, schema }))
+ store.topics.length === 0 &&
+ (await dispatch(fetchTopics({ init: true, page: 'schemas',
metalake, catalog, schema })))
+ await dispatch(setActivatedDetailsLoading(true))
await dispatch(getTopicDetails({ init: true, metalake, catalog,
schema, topic }))
- await dispatch(
+ await dispatch(setActivatedDetailsLoading(false))
+ dispatch(
getCurrentEntityTags({
init: true,
metalake,
@@ -230,7 +242,7 @@ const CatalogsListPage = () => {
details: true
})
)
- await dispatch(
+ dispatch(
getCurrentEntityPolicies({
init: true,
metalake,
@@ -241,10 +253,13 @@ const CatalogsListPage = () => {
)
break
case 'model':
- await dispatch(fetchModels({ init: true, page: 'schemas',
metalake, catalog, schema }))
+ store.models.length === 0 &&
+ (await dispatch(fetchModels({ init: true, page: 'schemas',
metalake, catalog, schema })))
await dispatch(fetchModelVersions({ init: true, metalake,
catalog, schema, model }))
+ await dispatch(setActivatedDetailsLoading(true))
await dispatch(getModelDetails({ init: true, metalake, catalog,
schema, model }))
- await dispatch(
+ await dispatch(setActivatedDetailsLoading(false))
+ dispatch(
getCurrentEntityTags({
init: true,
metalake,
@@ -253,7 +268,7 @@ const CatalogsListPage = () => {
details: true
})
)
- await dispatch(
+ dispatch(
getCurrentEntityPolicies({
init: true,
metalake,
@@ -273,9 +288,10 @@ const CatalogsListPage = () => {
await dispatch(fetchSchemas({ metalake, catalog, catalogType }))
await dispatch(fetchModels({ init: true, page: 'schemas',
metalake, catalog, schema }))
}
- dispatch(getVersionDetails({ init: true, metalake, catalog, schema,
model, version }))
+ await dispatch(setActivatedDetailsLoading(true))
+ await dispatch(getVersionDetails({ init: true, metalake, catalog,
schema, model, version }))
+ await dispatch(setActivatedDetailsLoading(false))
}
- await dispatch(setActivatedDetailsLoading(false))
}
}
fetchDependsData()
diff --git a/web-v2/web/src/app/catalogs/rightContent/CreateCatalogDialog.js
b/web-v2/web/src/app/catalogs/rightContent/CreateCatalogDialog.js
index d0cff6543e..c7eeafba5a 100644
--- a/web-v2/web/src/app/catalogs/rightContent/CreateCatalogDialog.js
+++ b/web-v2/web/src/app/catalogs/rightContent/CreateCatalogDialog.js
@@ -385,9 +385,9 @@ export default function CreateCatalogDialog({ ...props }) {
</div>
</div>
{provider.value === currentProvider ? (
- <Icons.CircleCheckBig className='provider-radio size-4 text-white
default-theme-radio' />
+ <Icons.CircleCheckBig className='provider-radio size-4 shrink-0
text-white default-theme-radio' />
) : (
- <Icons.Circle className='provider-radio size-4 text-gray-400
default-theme-radio' />
+ <Icons.Circle className='provider-radio size-4 shrink-0
text-gray-400 default-theme-radio' />
)}
</div>
)
diff --git a/web-v2/web/src/app/catalogs/rightContent/CreateSchemaDialog.js
b/web-v2/web/src/app/catalogs/rightContent/CreateSchemaDialog.js
index d9910e3b88..31f7065f2c 100644
--- a/web-v2/web/src/app/catalogs/rightContent/CreateSchemaDialog.js
+++ b/web-v2/web/src/app/catalogs/rightContent/CreateSchemaDialog.js
@@ -45,7 +45,18 @@ const defaultValues = {
}
export default function CreateSchemaDialog({ ...props }) {
- const { open, setOpen, metalake, catalog, catalogType, provider,
locationProviders, editSchema, init } = props
+ const {
+ open,
+ setOpen,
+ metalake,
+ catalog,
+ catalogType,
+ provider,
+ locationProviders,
+ catalogBackend,
+ editSchema,
+ init
+ } = props
const [confirmLoading, setConfirmLoading] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [cacheData, setCacheData] = useState()
@@ -59,6 +70,8 @@ export default function CreateSchemaDialog({ ...props }) {
const selectBefore = [...new Set(['file:/', 'hdfs://',
...currentSelectBefore])]
const dispatch = useAppDispatch()
+ const paimonCatalogBackend = provider === 'lakehouse-paimon' && ['hive',
'jdbc'].includes(catalogBackend)
+
const [form] = Form.useForm()
const values = Form.useWatch([], form)
@@ -219,12 +232,13 @@ export default function CreateSchemaDialog({ ...props }) {
>
<Input placeholder={mismatchName} disabled={editSchema} />
</Form.Item>
- {!['jdbc-mysql', 'lakehouse-paimon'].includes(provider) && (
+ {(!['jdbc-mysql', 'lakehouse-paimon',
'jdbc-oceanbase'].includes(provider) || paimonCatalogBackend) && (
<Form.Item name='comment' label='Comment'
data-refer='schema-comment-field'>
<TextArea disabled={editSchema} />
</Form.Item>
)}
- {!['jdbc-postgresql', 'lakehouse-paimon', 'kafka',
'jdbc-mysql'].includes(provider) && (
+ {(!['jdbc-postgresql', 'lakehouse-paimon', 'kafka',
'jdbc-mysql'].includes(provider) ||
+ paimonCatalogBackend) && (
<Form.Item label='Properties'>
<Form.List name='properties'>
{(fields, subOpt) => (
diff --git a/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js
b/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js
index 4a68258b77..f528acd635 100644
--- a/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js
+++ b/web-v2/web/src/app/catalogs/rightContent/CreateTableDialog.js
@@ -438,6 +438,7 @@ export default function CreateTableDialog({ ...props }) {
Object.entries(table.properties).forEach(([key, value]) => {
form.setFieldValue(['properties', idxProperty, 'key'], key)
form.setFieldValue(['properties', idxProperty, 'value'], value)
+ form.setFieldValue(['properties', idxProperty, 'isEdit'], true)
idxProperty++
})
}
diff --git
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/CatalogDetailsPage.js
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/CatalogDetailsPage.js
index 62add30ac8..f59bf91fa8 100644
---
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/CatalogDetailsPage.js
+++
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/CatalogDetailsPage.js
@@ -174,7 +174,7 @@ export default function CatalogDetailsPage() {
const tabOptions = anthEnable
? [
{ label: 'Schemas', key: 'Schemas' },
- { label: 'Associated roles', key: 'Associated roles' }
+ { label: 'Associated Roles', key: 'Associated Roles' }
]
: [{ label: 'Schemas', key: 'Schemas' }]
@@ -500,6 +500,7 @@ export default function CatalogDetailsPage() {
catalogType={catalogType}
provider={store.activatedDetails?.provider}
locationProviders={store.activatedDetails?.properties?.['filesystem-providers']?.split(',')
|| []}
+
catalogBackend={store.activatedDetails?.properties?.['catalog-backend']}
editSchema={editSchema}
/>
)}
diff --git
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FilesetDetailsPage.js
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FilesetDetailsPage.js
index 8214d49908..ad34e2136c 100644
---
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FilesetDetailsPage.js
+++
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FilesetDetailsPage.js
@@ -158,7 +158,7 @@ export default function FilesetDetailsPage({ ...props }) {
key: 'files',
disabled: disableFilesystemOps
},
- ...(anthEnable ? [{ label: 'Associated roles', key: 'Associated roles' }]
: [])
+ ...(anthEnable ? [{ label: 'Associated Roles', key: 'Associated Roles' }]
: [])
]
const handleEditTable = () => {
@@ -277,7 +277,7 @@ export default function FilesetDetailsPage({ ...props }) {
/>
</div>
)}
- {anthEnable && activeTab === 'Associated roles' && fileset && (
+ {anthEnable && activeTab === 'Associated Roles' && fileset && (
<AssociatedTable
metalake={currentMetalake}
metadataObjectType={'fileset'}
diff --git
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/ModelDetailsPage.js
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/ModelDetailsPage.js
index c6a5d95ed7..2b4e7148e6 100644
---
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/ModelDetailsPage.js
+++
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/ModelDetailsPage.js
@@ -24,6 +24,7 @@ import dynamic from 'next/dynamic'
import { ExclamationCircleFilled, PlusOutlined } from '@ant-design/icons'
import {
Button,
+ Descriptions,
Divider,
Drawer,
Empty,
@@ -134,7 +135,7 @@ export default function ModelDetailsPage({ ...props }) {
const tabOptions = [
{ label: 'Versions', key: 'Versions' },
- ...(anthEnable ? [{ label: 'Associated roles', key: 'Associated roles' }]
: [])
+ ...(anthEnable ? [{ label: 'Associated Roles', key: 'Associated Roles' }]
: [])
]
const onChangeTab = key => {
@@ -368,90 +369,77 @@ export default function ModelDetailsPage({ ...props }) {
/>
)}
{openVersion && (
- <Drawer title={`Version ${currentVersion?.version} Details`}
onClose={onClose} open={openVersion}>
- <>
- {currentVersion?.uri && (
- <div className='my-4'>
- <div className='text-sm text-slate-400'>URI</div>
- <span className='break-words
text-base'>{currentVersion?.uri}</span>
- </div>
- )}
+ <Drawer
+ title={`Version ${currentVersion?.version} Details`}
+ onClose={onClose}
+ open={openVersion}
+ width={560}
+ >
+ <Title level={5} className='mb-2'>
+ Basic Information
+ </Title>
+ <Descriptions column={1} bordered size='small'>
+ {currentVersion?.uri && <Descriptions.Item
label='URI'>{currentVersion?.uri}</Descriptions.Item>}
{currentVersion?.uris && (
- <div className='my-4'>
- <div className='mb-1 text-sm text-slate-400'>URI(s)</div>
+ <Descriptions.Item label='URI(s)'>
<Space.Compact className='max-h-80 w-full overflow-auto'>
<Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
<span className='bg-gray-100 p-1'>URI Name</span>
- {currentVersion?.uris
- ? Object.keys(currentVersion?.uris).map(name => (
- <span key={name} className='truncate p-1'
title={name}>
- {name}
- </span>
- ))
- : null}
+ {Object.keys(currentVersion.uris).map(name => (
+ <span key={name} className='truncate p-1'
title={name}>
+ {name}
+ </span>
+ ))}
</Space.Compact>
<Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
<span className='bg-gray-100 p-1'>URI</span>
- {currentVersion?.uris
- ? Object.values(currentVersion?.uris).map(uri => (
- <span key={uri} className='truncate p-1'
title={uri}>
- {uri || '-'}
- </span>
- ))
- : null}
+ {Object.values(currentVersion.uris).map((uri, idx) => (
+ <span key={idx} className='truncate p-1' title={uri}>
+ {uri || '-'}
+ </span>
+ ))}
</Space.Compact>
</Space.Compact>
- </div>
+ </Descriptions.Item>
)}
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Aliases</div>
- <span className='break-words text-base'>
- {currentVersion?.aliases.length === 1
- ? currentVersion?.aliases[0]
- : currentVersion?.aliases.length
- ? currentVersion?.aliases.join(', ')
- : '-'}
- </span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Comment</div>
- <span className='break-words
text-base'>{currentVersion?.comment || '-'}</span>
- </div>
- <div className='my-4'>
- <div className='mb-1 text-sm text-slate-400'>Properties</div>
- {currentVersion?.properties &&
Object.keys(currentVersion?.properties).length > 0 ? (
+ <Descriptions.Item label='Aliases'>
+ {currentVersion?.aliases?.length === 1
+ ? currentVersion?.aliases[0]
+ : currentVersion?.aliases?.length
+ ? currentVersion?.aliases.join(', ')
+ : '-'}
+ </Descriptions.Item>
+ <Descriptions.Item label='Comment'>{currentVersion?.comment ||
'-'}</Descriptions.Item>
+ <Descriptions.Item label='Properties'>
+ {currentVersion?.properties &&
Object.keys(currentVersion.properties).length > 0 ? (
<Space.Compact className='max-h-80 w-full overflow-auto'>
<Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
<span className='bg-gray-100 p-1'>Key</span>
- {currentVersion?.properties
- ? Object.keys(currentVersion?.properties).map(key =>
(
- <span key={key} className='truncate p-1'
title={key}>
- {key}
- </span>
- ))
- : null}
+ {Object.keys(currentVersion.properties).map(key => (
+ <span key={key} className='truncate p-1' title={key}>
+ {key}
+ </span>
+ ))}
</Space.Compact>
<Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
<span className='bg-gray-100 p-1'>Value</span>
- {currentVersion?.properties
- ?
Object.values(currentVersion?.properties).map(value => (
- <span key={value} className='truncate p-1'
title={value}>
- {value || '-'}
- </span>
- ))
- : null}
+ {Object.values(currentVersion.properties).map((value,
idx) => (
+ <span key={idx} className='truncate p-1'
title={value}>
+ {value || '-'}
+ </span>
+ ))}
</Space.Compact>
</Space.Compact>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
- </div>
- </>
+ </Descriptions.Item>
+ </Descriptions>
</Drawer>
)}
</>
)}
- {anthEnable && activeTab === 'Associated roles' && model && (
+ {anthEnable && activeTab === 'Associated Roles' && model && (
<AssociatedTable
metalake={currentMetalake}
metadataObjectType={'model'}
diff --git
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/SchemaDetailsPage.js
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/SchemaDetailsPage.js
index f1f96bf72c..38555281d6 100644
---
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/SchemaDetailsPage.js
+++
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/SchemaDetailsPage.js
@@ -127,7 +127,7 @@ export default function SchemaDetailsPage() {
? [
{ label: 'Tables', key: 'Tables' },
{ label: 'Functions', key: 'Functions' },
- { label: 'Associated roles', key: 'Associated roles' }
+ { label: 'Associated Roles', key: 'Associated Roles' }
]
: [
{ label: 'Tables', key: 'Tables' },
@@ -145,7 +145,7 @@ export default function SchemaDetailsPage() {
? [
{ label: 'Topics', key: 'Topics' },
{ label: 'Functions', key: 'Functions' },
- { label: 'Associated roles', key: 'Associated roles' }
+ { label: 'Associated Roles', key: 'Associated Roles' }
]
: [
{ label: 'Topics', key: 'Topics' },
@@ -163,7 +163,7 @@ export default function SchemaDetailsPage() {
? [
{ label: 'Filesets', key: 'Filesets' },
{ label: 'Functions', key: 'Functions' },
- { label: 'Associated roles', key: 'Associated roles' }
+ { label: 'Associated Roles', key: 'Associated Roles' }
]
: [
{ label: 'Filesets', key: 'Filesets' },
@@ -181,7 +181,7 @@ export default function SchemaDetailsPage() {
? [
{ label: 'Models', key: 'Models' },
{ label: 'Functions', key: 'Functions' },
- { label: 'Associated roles', key: 'Associated roles' }
+ { label: 'Associated Roles', key: 'Associated Roles' }
]
: [
{ label: 'Models', key: 'Models' },
@@ -536,7 +536,7 @@ export default function SchemaDetailsPage() {
</Space>
</Spin>
<Tabs data-refer='details-tabs' defaultActiveKey={tabKey}
onChange={onChangeTab} items={tabOptions} />
- {tabKey === 'Associated roles' ? (
+ {tabKey === 'Associated Roles' ? (
<AssociatedTable
metalake={currentMetalake}
metadataObjectType={'schema'}
diff --git
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TableDetailsPage.js
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TableDetailsPage.js
index 988392c9a6..3567f0cc64 100644
---
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TableDetailsPage.js
+++
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TableDetailsPage.js
@@ -345,7 +345,7 @@ export default function TableDetailsPage({ ...props }) {
const tabOptions = [
{ label: 'Columns', key: 'Columns' },
- ...(anthEnable ? [{ label: 'Associated roles', key: 'Associated roles' }]
: [])
+ ...(anthEnable ? [{ label: 'Associated Roles', key: 'Associated Roles' }]
: [])
]
const onChangeTab = key => {
@@ -613,7 +613,7 @@ export default function TableDetailsPage({ ...props }) {
</Spin>
</>
)}
- {tabKey === 'Associated roles' && anthEnable && (
+ {tabKey === 'Associated Roles' && anthEnable && (
<AssociatedTable
metalake={currentMetalake}
metadataObjectType={'table'}
diff --git
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TopicDetailsPage.js
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TopicDetailsPage.js
index c2d08e4eaa..eb97f93729 100644
---
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TopicDetailsPage.js
+++
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/TopicDetailsPage.js
@@ -92,7 +92,7 @@ export default function TopicDetailsPage({ ...props }) {
const propertyContent = (
<PropertiesContent properties={properties} dataReferPrefix='props'
contentDataRefer='properties-popover-content' />
)
- const tabOptions = [{ label: 'Associated roles', key: 'Associated roles' }]
+ const tabOptions = [{ label: 'Associated Roles', key: 'Associated Roles' }]
const handleEditTable = () => {
setOpen(true)
diff --git a/web-v2/web/src/app/compliance/policies/page.js
b/web-v2/web/src/app/compliance/policies/page.js
index e64538ba77..ed8dda2f1a 100644
--- a/web-v2/web/src/app/compliance/policies/page.js
+++ b/web-v2/web/src/app/compliance/policies/page.js
@@ -24,6 +24,7 @@ import { useRouter, useSearchParams } from 'next/navigation'
import { ExclamationCircleFilled, PlusOutlined, StopOutlined } from
'@ant-design/icons'
import {
Button,
+ Descriptions,
Drawer,
Dropdown,
Empty,
@@ -43,6 +44,7 @@ import { useAntdColumnResize } from 'react-antd-column-resize'
import ConfirmInput from '@/components/ConfirmInput'
import Icons from '@/components/Icons'
import SectionContainer from '@/components/SectionContainer'
+import AssociatedTable from '@/components/AssociatedTable'
import CreatePolicyDialog from './CreatePolicyDialog'
import { formatToDateTime } from '@/lib/utils'
import { useAppSelector, useAppDispatch } from '@/lib/hooks/useStore'
@@ -66,6 +68,8 @@ export default function PoliciesPage() {
const searchParams = useSearchParams()
const currentMetalake = searchParams.get('metalake')
const [detailsLoading, setDetailsLoading] = useState(false)
+ const auth = useAppSelector(state => state.auth)
+ const { anthEnable } = auth
useEffect(() => {
currentMetalake && dispatch(fetchPolicies({ metalake: currentMetalake,
details: true }))
@@ -277,95 +281,112 @@ export default function PoliciesPage() {
</Spin>
{openPolicy && (
<Drawer
- title={`View ${currentPolicy?.name} details`}
+ title={`View Policy "${currentPolicy?.name}" Details`}
loading={detailsLoading}
onClose={onClose}
open={openPolicy}
+ width={'40%'}
>
- <>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Policy Name</div>
- <span className='break-words
text-base'>{currentPolicy?.name}</span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Enabled</div>
- <span className='break-words text-base'>
- <Switch checked={currentPolicy?.enabled} disabled size='small'
/>
- </span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Policy Type</div>
- <span className='break-words
text-base'>{currentPolicy?.policyType}</span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Supported Object
Types</div>
- <span className='break-words text-base'>
- {currentPolicy?.content?.supportedObjectTypes.length === 1
- ? currentPolicy?.content?.supportedObjectTypes[0]
- : currentPolicy?.content?.supportedObjectTypes.length
- ? currentPolicy?.content?.supportedObjectTypes.join(', ')
- : '-'}
- </span>
- </div>
- <div className='my-4'>
- <div className='mb-1 text-sm text-slate-400'>Rule(s)</div>
- <Space.Compact className='max-h-80 w-full overflow-auto'>
- <Space.Compact direction='vertical' className='w-1/2 divide-y
border-gray-100'>
- <span className='bg-gray-100 p-1'>Rule Name</span>
- {currentPolicy?.content?.customRules
- ?
Object.keys(currentPolicy?.content?.customRules).map(rule => (
- <span key={rule} className='truncate p-1' title={rule}>
- {rule}
- </span>
- ))
- : null}
- </Space.Compact>
- <Space.Compact direction='vertical' className='w-1/2 divide-y
border-gray-100'>
- <span className='bg-gray-100 p-1'>Rule Content</span>
- {currentPolicy?.content?.customRules
- ?
Object.values(currentPolicy?.content?.customRules).map(ruleContent => (
- <span key={ruleContent} className='truncate p-1'
title={ruleContent}>
- {ruleContent || '-'}
- </span>
- ))
- : null}
- </Space.Compact>
- </Space.Compact>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Comment</div>
- <span className='break-words text-base'>{currentPolicy?.comment
|| '-'}</span>
- </div>
- <div className='my-4'>
- <div className='mb-1 text-sm text-slate-400'>Properties</div>
- {currentPolicy?.content?.properties &&
Object.keys(currentPolicy?.content?.properties).length > 0 ? (
- <Space.Compact className='max-h-80 w-full overflow-auto'>
- <Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
- <span className='bg-gray-100 p-1'>Key</span>
- {currentPolicy?.content?.properties
- ?
Object.keys(currentPolicy?.content?.properties).map(key => (
- <span key={key} className='truncate p-1' title={key}>
- {key}
- </span>
- ))
- : null}
- </Space.Compact>
- <Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
- <span className='bg-gray-100 p-1'>Value</span>
- {currentPolicy?.content?.properties
- ?
Object.values(currentPolicy?.content?.properties).map(value => (
- <span key={value} className='truncate p-1'
title={value}>
- {value || '-'}
- </span>
- ))
- : null}
- </Space.Compact>
- </Space.Compact>
- ) : (
- <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
- )}
- </div>
- </>
+ <Title level={5} className='mb-2'>
+ Basic Information
+ </Title>
+ <Descriptions column={1} bordered size='small'>
+ <Descriptions.Item label='Policy
Name'>{currentPolicy?.name}</Descriptions.Item>
+ <Descriptions.Item label='Enabled'>
+ <Switch checked={currentPolicy?.enabled} disabled size='small' />
+ </Descriptions.Item>
+ <Descriptions.Item label='Policy
Type'>{currentPolicy?.policyType}</Descriptions.Item>
+ <Descriptions.Item label='Supported Object Types'>
+ {currentPolicy?.content?.supportedObjectTypes?.length === 1
+ ? currentPolicy?.content?.supportedObjectTypes[0]
+ : currentPolicy?.content?.supportedObjectTypes?.length
+ ? currentPolicy?.content?.supportedObjectTypes.join(', ')
+ : '-'}
+ </Descriptions.Item>
+ <Descriptions.Item label='Comment'>{currentPolicy?.comment ||
'-'}</Descriptions.Item>
+ <Descriptions.Item label='Creator'>{currentPolicy?.audit?.creator
|| '-'}</Descriptions.Item>
+ <Descriptions.Item label='Created At'>
+ {currentPolicy?.audit?.createTime ?
formatToDateTime(currentPolicy.audit.createTime) : '-'}
+ </Descriptions.Item>
+ <Descriptions.Item label='Last Modified At'>
+ {currentPolicy?.audit?.lastModifiedTime ?
formatToDateTime(currentPolicy.audit.lastModifiedTime) : '-'}
+ </Descriptions.Item>
+ <Descriptions.Item label='Last
Modifier'>{currentPolicy?.audit?.lastModifier || '-'}</Descriptions.Item>
+ </Descriptions>
+ <Title level={5} className='mt-4 mb-2'>
+ Rule(s)
+ </Title>
+ <Table
+ size='small'
+ pagination={false}
+ rowKey='name'
+ dataSource={
+ currentPolicy?.content?.customRules &&
Object.keys(currentPolicy.content.customRules).length > 0
+ ?
Object.entries(currentPolicy.content.customRules).map(([name, content]) => ({
+ name,
+ content
+ }))
+ : []
+ }
+ columns={[
+ {
+ title: 'Rule Name',
+ dataIndex: 'name',
+ key: 'name',
+ ellipsis: true
+ },
+ {
+ title: 'Rule Content',
+ dataIndex: 'content',
+ key: 'content',
+ ellipsis: true,
+ render: value => value || '-'
+ }
+ ]}
+ />
+ <Title level={5} className='mt-4 mb-2'>
+ Properties
+ </Title>
+ <Table
+ size='small'
+ pagination={false}
+ rowKey='key'
+ dataSource={
+ currentPolicy?.content?.properties &&
Object.keys(currentPolicy.content.properties).length > 0
+ ? Object.entries(currentPolicy.content.properties).map(([key,
value]) => ({
+ key,
+ value
+ }))
+ : []
+ }
+ columns={[
+ {
+ title: 'Key',
+ dataIndex: 'key',
+ key: 'key',
+ ellipsis: true
+ },
+ {
+ title: 'Value',
+ dataIndex: 'value',
+ key: 'value',
+ ellipsis: true,
+ render: value => value || '-'
+ }
+ ]}
+ />
+ {anthEnable && (
+ <>
+ <Title level={5} className='mt-4 mb-2'>
+ Associated Roles
+ </Title>
+ <AssociatedTable
+ metalake={currentMetalake}
+ metadataObjectType={'policy'}
+ metadataObjectFullName={currentPolicy?.name}
+ />
+ </>
+ )}
</Drawer>
)}
<CreatePolicyDialog open={open} setOpen={setOpen}
metalake={currentMetalake} editPolicy={editPolicy} />
diff --git a/web-v2/web/src/app/compliance/tags/page.js
b/web-v2/web/src/app/compliance/tags/page.js
index 6599ff3c66..74a27f0644 100644
--- a/web-v2/web/src/app/compliance/tags/page.js
+++ b/web-v2/web/src/app/compliance/tags/page.js
@@ -22,11 +22,12 @@
import { createContext, useMemo, useState, useEffect } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { ExclamationCircleFilled, PlusOutlined } from '@ant-design/icons'
-import { Button, Flex, Input, Modal, Spin, Table, Tag, Tooltip, Typography }
from 'antd'
+import { Button, Descriptions, Drawer, Flex, Input, Modal, Spin, Table, Tag,
Tooltip, Typography } from 'antd'
import { useAntdColumnResize } from 'react-antd-column-resize'
import ConfirmInput from '@/components/ConfirmInput'
import Icons from '@/components/Icons'
import SectionContainer from '@/components/SectionContainer'
+import AssociatedTable from '@/components/AssociatedTable'
import CreateTagDialog from './CreateTagDialog'
import { formatToDateTime } from '@/lib/utils'
import { useAppSelector, useAppDispatch } from '@/lib/hooks/useStore'
@@ -40,12 +41,16 @@ export default function TagsPage() {
const [open, setOpen] = useState(false)
const [editTag, setEditTag] = useState('')
const [search, setSearch] = useState('')
+ const [drawerOpen, setDrawerOpen] = useState(false)
+ const [selectedTag, setSelectedTag] = useState(null)
const [modal, contextHolder] = Modal.useModal()
const router = useRouter()
const dispatch = useAppDispatch()
const store = useAppSelector(state => state.tags)
const searchParams = useSearchParams()
const currentMetalake = searchParams.get('metalake')
+ const auth = useAppSelector(state => state.auth)
+ const { anthEnable } = auth
useEffect(() => {
currentMetalake && dispatch(fetchTags({ metalake: currentMetalake,
details: true }))
@@ -118,6 +123,16 @@ export default function TagsPage() {
router.push(`/metadataObjectsForTag?tag=${name}&metalake=${currentMetalake}`)
}
+ const handleViewTag = record => {
+ setSelectedTag(record)
+ setDrawerOpen(true)
+ }
+
+ const handleCloseDrawer = () => {
+ setDrawerOpen(false)
+ setSelectedTag(null)
+ }
+
const columns = useMemo(
() => [
{
@@ -159,7 +174,7 @@ export default function TagsPage() {
{
title: 'Actions',
key: 'action',
- width: 100,
+ width: 140,
render: (_, record) => {
const NameContext = createContext(record.name)
@@ -171,6 +186,11 @@ export default function TagsPage() {
<Icons.Pencil className='size-4' onClick={() =>
handleEditTag(record.name)} />
</Tooltip>
</a>
+ <a>
+ <Tooltip title='View'>
+ <Icons.Eye className='size-4' onClick={() =>
handleViewTag(record)} />
+ </Tooltip>
+ </a>
<a>
<Tooltip title='Delete'>
<Icons.Trash2Icon className='size-4' onClick={() =>
showDeleteConfirm(NameContext, record.name)} />
@@ -211,6 +231,43 @@ export default function TagsPage() {
/>
</Spin>
<CreateTagDialog open={open} setOpen={setOpen}
metalake={currentMetalake} editTag={editTag} />
+ <Drawer
+ title={`View Tag "${selectedTag?.name || ''}" Details`}
+ placement='right'
+ width={'40%'}
+ onClose={handleCloseDrawer}
+ open={drawerOpen}
+ >
+ {selectedTag && (
+ <>
+ <Title level={5} className='mb-2'>
+ Basic Information
+ </Title>
+ <Descriptions column={1} bordered size='small'>
+ <Descriptions.Item label='Tag Name'>
+ <Tag
color={selectedTag.properties?.color}>{selectedTag.name}</Tag>
+ </Descriptions.Item>
+ <Descriptions.Item label='Comment'>{selectedTag.comment ||
'-'}</Descriptions.Item>
+ <Descriptions.Item label='Created At'>
+ {selectedTag.createTime ?
formatToDateTime(selectedTag.createTime) : '-'}
+ </Descriptions.Item>
+ <Descriptions.Item label='Creator'>{selectedTag.audit?.creator
|| '-'}</Descriptions.Item>
+ </Descriptions>
+ {anthEnable && (
+ <>
+ <Title level={5} className='mt-4 mb-2'>
+ Associated Roles
+ </Title>
+ <AssociatedTable
+ metalake={currentMetalake}
+ metadataObjectType={'tag'}
+ metadataObjectFullName={selectedTag?.name}
+ />
+ </>
+ )}
+ </>
+ )}
+ </Drawer>
</SectionContainer>
)
}
diff --git a/web-v2/web/src/app/jobTemplates/page.js
b/web-v2/web/src/app/jobTemplates/page.js
index f5b38dbdf2..5da6404b08 100644
--- a/web-v2/web/src/app/jobTemplates/page.js
+++ b/web-v2/web/src/app/jobTemplates/page.js
@@ -23,11 +23,12 @@ import { createContext, useContext, useEffect, useMemo,
useState } from 'react'
import dynamic from 'next/dynamic'
import { useRouter, useSearchParams } from 'next/navigation'
import { ExclamationCircleFilled, PlusOutlined } from '@ant-design/icons'
-import { Button, Drawer, Flex, Input, Modal, Space, Spin, Table, Tooltip,
Typography } from 'antd'
+import { Button, Descriptions, Drawer, Flex, Input, Modal, Spin, Table, Tag,
Tooltip, Typography } from 'antd'
import { useAntdColumnResize } from 'react-antd-column-resize'
import ConfirmInput from '@/components/ConfirmInput'
import Icons from '@/components/Icons'
import SectionContainer from '@/components/SectionContainer'
+import AssociatedTable from '@/components/AssociatedTable'
import { formatToDateTime } from '@/lib/utils'
import Loading from '@/components/Loading'
import { useAppSelector, useAppDispatch } from '@/lib/hooks/useStore'
@@ -55,6 +56,8 @@ export default function JobTemplatesPage() {
const dispatch = useAppDispatch()
const store = useAppSelector(state => state.jobs)
const [isLoadingDetails, setIsLoadingDetails] = useState(false)
+ const auth = useAppSelector(state => state.auth)
+ const { anthEnable } = auth
useEffect(() => {
currentMetalake && dispatch(fetchJobTemplates({ metalake: currentMetalake,
details: true }))
@@ -251,187 +254,232 @@ export default function JobTemplatesPage() {
</Spin>
{openDetailJobTemplate && (
<Drawer
- title={`View Details - ${currentJobTemplate?.name || ''}`}
+ title={`View Job Template "${currentJobTemplate?.name || ''}"
Details`}
loading={isLoadingDetails}
onClose={onClose}
open={openDetailJobTemplate}
+ width={'40%'}
>
- <>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Template Name</div>
- <span className='break-words
text-base'>{currentJobTemplate?.name}</span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Job Type</div>
- <span className='break-words
text-base'>{currentJobTemplate?.jobType}</span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Executable</div>
- <span className='break-words
text-base'>{currentJobTemplate?.executable}</span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Comment</div>
- <span className='break-words
text-base'>{currentJobTemplate?.comment || '-'}</span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Arguments</div>
- <span className='break-words text-base'>
- {currentJobTemplate?.arguments.length === 1
- ? currentJobTemplate?.arguments[0]
- : currentJobTemplate?.arguments.length
- ? currentJobTemplate?.arguments.join(', ')
- : '-'}
- </span>
- </div>
- <div className='my-4'>
- <div className='mb-1 text-sm text-slate-400'>Environment
Variable(s)</div>
- <Space.Compact className='max-h-80 w-full overflow-auto'>
- <Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
- <span className='bg-gray-100 p-1'>Env Var Name</span>
- {currentJobTemplate?.environments
- ?
Object.keys(currentJobTemplate?.environments).map(envName => (
- <span key={envName} className='truncate p-1'
title={envName}>
- {envName}
- </span>
- ))
- : null}
- </Space.Compact>
- <Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
- <span className='bg-gray-100 p-1'>Env Var Value</span>
- {currentJobTemplate?.environments
- ?
Object.values(currentJobTemplate?.environments).map(envValue => (
- <span key={envValue} className='truncate p-1'
title={envValue}>
- {envValue || '-'}
- </span>
- ))
- : null}
- </Space.Compact>
- </Space.Compact>
- </div>
- <div className='my-4'>
- <div className='mb-1 text-sm text-slate-400'>Custom
Fields</div>
- <Space.Compact className='max-h-80 w-full overflow-auto'>
- <Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
- <span className='bg-gray-100 p-1'>Key</span>
- {currentJobTemplate?.customFields
- ?
Object.keys(currentJobTemplate?.customFields).map(field => (
- <span key={field} className='truncate p-1'
title={field}>
- {field}
- </span>
- ))
- : null}
- </Space.Compact>
- <Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
- <span className='bg-gray-100 p-1'>Value</span>
- {currentJobTemplate?.customFields
- ?
Object.values(currentJobTemplate?.customFields).map(value => (
- <span key={value} className='truncate p-1'
title={value}>
- {value || '-'}
- </span>
- ))
- : null}
- </Space.Compact>
- </Space.Compact>
- </div>
- {currentJobTemplate?.jobType === 'shell' && (
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Script(s)</div>
- {currentJobTemplate?.scripts &&
currentJobTemplate.scripts.length > 0 ? (
- currentJobTemplate.scripts.map((script, index) => (
- <span key={index} className='mb-2 block break-words
last:mb-0'>
- {script}
- </span>
- ))
- ) : (
- <span>-</span>
- )}
- </div>
- )}
+ <Title level={5} className='mb-2'>
+ Basic Information
+ </Title>
+ <Descriptions column={1} bordered size='small'>
+ <Descriptions.Item label='Template
Name'>{currentJobTemplate?.name}</Descriptions.Item>
+ <Descriptions.Item label='Job
Type'>{currentJobTemplate?.jobType}</Descriptions.Item>
+ <Descriptions.Item
label='Executable'>{currentJobTemplate?.executable}</Descriptions.Item>
+ <Descriptions.Item label='Comment'>{currentJobTemplate?.comment
|| '-'}</Descriptions.Item>
{currentJobTemplate?.jobType === 'spark' && (
- <>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Class Name</div>
- <span className='break-words
text-base'>{currentJobTemplate?.className}</span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Jars</div>
- {currentJobTemplate?.jars &&
currentJobTemplate.jars.length > 0 ? (
- currentJobTemplate.jars.map((jar, index) => (
- <span key={index} className='mb-2 block break-words
last:mb-0'>
- {jar}
- </span>
- ))
- ) : (
- <span>-</span>
- )}
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Files</div>
- {currentJobTemplate?.files &&
currentJobTemplate.files.length > 0 ? (
- currentJobTemplate.files.map((file, index) => (
- <span key={index} className='mb-2 block break-words
last:mb-0'>
- {file}
- </span>
- ))
- ) : (
- <span>-</span>
- )}
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Archives</div>
- {currentJobTemplate?.archives &&
currentJobTemplate.archives.length > 0 ? (
- currentJobTemplate.archives.map((archive, index) => (
+ <Descriptions.Item label='Class
Name'>{currentJobTemplate?.className || '-'}</Descriptions.Item>
+ )}
+ <Descriptions.Item label='Arguments'>
+ {currentJobTemplate?.arguments?.length
+ ? currentJobTemplate.arguments.map((argument, index) => (
+ <Tag key={`${argument}-${index}`} className='mb-1 mr-1'>
+ {argument}
+ </Tag>
+ ))
+ : '-'}
+ </Descriptions.Item>
+ {currentJobTemplate?.jobType === 'shell' && (
+ <Descriptions.Item label='Script(s)'>
+ {currentJobTemplate?.scripts &&
currentJobTemplate.scripts.length > 0
+ ? currentJobTemplate.scripts.map((script, index) => (
<span key={index} className='mb-2 block break-words
last:mb-0'>
- {archive}
+ {script}
</span>
))
- ) : (
- <span>-</span>
- )}
- </div>
- <div className='my-4'>
- <div className='mb-1 text-sm
text-slate-400'>Config(s)</div>
- <Space.Compact className='max-h-80 w-full overflow-auto'>
- <Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
- <span className='bg-gray-100 p-1'>Config Name</span>
- {currentJobTemplate?.configs
- ?
Object.keys(currentJobTemplate?.configs).map(configName => (
- <span key={configName} className='truncate p-1'
title={configName}>
- {configName}
- </span>
- ))
- : null}
- </Space.Compact>
- <Space.Compact direction='vertical' className='w-1/2
divide-y border-gray-100'>
- <span className='bg-gray-100 p-1'>Config Value</span>
- {currentJobTemplate?.configs
- ?
Object.values(currentJobTemplate?.configs).map(configValue => (
- <span key={configValue} className='truncate p-1'
title={configValue}>
- {configValue || '-'}
- </span>
- ))
- : null}
- </Space.Compact>
- </Space.Compact>
- </div>
- </>
+ : '-'}
+ </Descriptions.Item>
)}
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Details</div>
- <div className='flex justify-between'>
- <span className='text-sm'>Creator: </span>
- <span
className='text-sm'>{currentJobTemplate?.audit?.creator}</span>
- </div>
- <div className='flex justify-between'>
- <span className='text-sm'>Created At: </span>
- <span
className='text-sm'>{formatToDateTime(currentJobTemplate?.audit?.createTime)}</span>
- </div>
- <div className='flex justify-between'>
- <span className='text-sm'>Updated At: </span>
- <span
className='text-sm'>{formatToDateTime(currentJobTemplate?.audit?.lastModifiedTime)}</span>
- </div>
- </div>
- </>
+ </Descriptions>
+ <Title level={5} className='mt-4 mb-2'>
+ Environment Variable(s)
+ </Title>
+ <Table
+ size='small'
+ pagination={false}
+ rowKey='key'
+ dataSource={
+ currentJobTemplate?.environments &&
Object.keys(currentJobTemplate.environments).length > 0
+ ? Object.entries(currentJobTemplate.environments).map(([key,
value]) => ({
+ key,
+ value
+ }))
+ : []
+ }
+ columns={[
+ {
+ title: 'Env Var Name',
+ dataIndex: 'key',
+ key: 'key',
+ ellipsis: true
+ },
+ {
+ title: 'Env Var Value',
+ dataIndex: 'value',
+ key: 'value',
+ ellipsis: true,
+ render: value => value || '-'
+ }
+ ]}
+ />
+ <Title level={5} className='mt-4 mb-2'>
+ Custom Fields
+ </Title>
+ <Table
+ size='small'
+ pagination={false}
+ rowKey='key'
+ dataSource={
+ currentJobTemplate?.customFields &&
Object.keys(currentJobTemplate.customFields).length > 0
+ ? Object.entries(currentJobTemplate.customFields).map(([key,
value]) => ({
+ key,
+ value
+ }))
+ : []
+ }
+ columns={[
+ {
+ title: 'Key',
+ dataIndex: 'key',
+ key: 'key',
+ ellipsis: true
+ },
+ {
+ title: 'Value',
+ dataIndex: 'value',
+ key: 'value',
+ ellipsis: true,
+ render: value => value || '-'
+ }
+ ]}
+ />
+ {currentJobTemplate?.jobType === 'spark' && (
+ <>
+ <Title level={5} className='mt-4 mb-2'>
+ Jars
+ </Title>
+ <Table
+ size='small'
+ pagination={false}
+ rowKey='value'
+ dataSource={
+ currentJobTemplate?.jars && currentJobTemplate.jars.length
> 0
+ ? currentJobTemplate.jars.map(value => ({ value }))
+ : []
+ }
+ columns={[
+ {
+ title: 'Jar',
+ dataIndex: 'value',
+ key: 'value',
+ ellipsis: true
+ }
+ ]}
+ />
+ <Title level={5} className='mt-4 mb-2'>
+ Files
+ </Title>
+ <Table
+ size='small'
+ pagination={false}
+ rowKey='value'
+ dataSource={
+ currentJobTemplate?.files &&
currentJobTemplate.files.length > 0
+ ? currentJobTemplate.files.map(value => ({ value }))
+ : []
+ }
+ columns={[
+ {
+ title: 'File',
+ dataIndex: 'value',
+ key: 'value',
+ ellipsis: true
+ }
+ ]}
+ />
+ <Title level={5} className='mt-4 mb-2'>
+ Archives
+ </Title>
+ <Table
+ size='small'
+ pagination={false}
+ rowKey='value'
+ dataSource={
+ currentJobTemplate?.archives &&
currentJobTemplate.archives.length > 0
+ ? currentJobTemplate.archives.map(value => ({ value }))
+ : []
+ }
+ columns={[
+ {
+ title: 'Archive',
+ dataIndex: 'value',
+ key: 'value',
+ ellipsis: true
+ }
+ ]}
+ />
+ <Title level={5} className='mt-4 mb-2'>
+ Config(s)
+ </Title>
+ <Table
+ size='small'
+ pagination={false}
+ rowKey='key'
+ dataSource={
+ currentJobTemplate?.configs &&
Object.keys(currentJobTemplate.configs).length > 0
+ ? Object.entries(currentJobTemplate.configs).map(([key,
value]) => ({
+ key,
+ value
+ }))
+ : []
+ }
+ columns={[
+ {
+ title: 'Config Name',
+ dataIndex: 'key',
+ key: 'key',
+ ellipsis: true
+ },
+ {
+ title: 'Config Value',
+ dataIndex: 'value',
+ key: 'value',
+ ellipsis: true,
+ render: value => value || '-'
+ }
+ ]}
+ />
+ </>
+ )}
+ <div className='my-4'>
+ <Typography.Title level={5} className='mb-2'>
+ Details
+ </Typography.Title>
+ <Descriptions size='small' bordered column={1} labelStyle={{
width: 180 }}>
+ <Descriptions.Item
label='Creator'>{currentJobTemplate?.audit?.creator || '-'}</Descriptions.Item>
+ <Descriptions.Item label='Created At'>
+ {currentJobTemplate?.audit?.createTime ?
formatToDateTime(currentJobTemplate.audit.createTime) : '-'}
+ </Descriptions.Item>
+ <Descriptions.Item label='Updated At'>
+ {currentJobTemplate?.audit?.lastModifiedTime
+ ?
formatToDateTime(currentJobTemplate.audit.lastModifiedTime)
+ : '-'}
+ </Descriptions.Item>
+ </Descriptions>
+ </div>
+ {anthEnable && (
+ <>
+ <Title level={5} className='mt-4 mb-2'>
+ Associated Roles
+ </Title>
+ <AssociatedTable
+ metalake={currentMetalake}
+ metadataObjectType={'job_template'}
+ metadataObjectFullName={currentJobTemplate?.name}
+ />
+ </>
+ )}
</Drawer>
)}
{open && (
diff --git a/web-v2/web/src/app/jobs/page.js b/web-v2/web/src/app/jobs/page.js
index edd34cf211..bd27ace228 100644
--- a/web-v2/web/src/app/jobs/page.js
+++ b/web-v2/web/src/app/jobs/page.js
@@ -22,7 +22,7 @@ import { createContext, useContext, useMemo, useState,
useEffect } from 'react'
import dynamic from 'next/dynamic'
import { useRouter, useSearchParams } from 'next/navigation'
import { ArrowRightOutlined, ExclamationCircleFilled, PlusOutlined,
RedoOutlined } from '@ant-design/icons'
-import { Button, Drawer, Flex, Input, Modal, Spin, Table, Tag, Tooltip,
Typography, theme } from 'antd'
+import { Button, Descriptions, Drawer, Flex, Input, Modal, Spin, Table, Tag,
Tooltip, Typography, theme } from 'antd'
import { useAntdColumnResize } from 'react-antd-column-resize'
import ConfirmInput from '@/components/ConfirmInput'
import Icons from '@/components/Icons'
@@ -322,42 +322,29 @@ export default function JobsPage() {
</Spin>
{openDetailJob && (
<Drawer
- title={`View Job ${currentJob?.jobId} Details`}
+ title={`View Job "${currentJob?.jobId}" Details`}
loading={jobDetailLoading}
onClose={onClose}
open={openDetailJob}
+ width={'40%'}
>
- <>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Job ID</div>
- <span className='break-words
text-base'>{currentJob?.jobId}</span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Template Name</div>
- <span className='break-words
text-base'>{currentJob?.jobTemplateName}</span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Status</div>
- <span className='break-words text-base'>
- {<Tag color={getStatusColor(currentJob?.status ||
'')}>{currentJob?.status}</Tag>}
- </span>
- </div>
- <div className='my-4'>
- <div className='text-sm text-slate-400'>Details</div>
- <div className='flex justify-between'>
- <span className='text-sm'>Creator: </span>
- <span className='text-sm'>{currentJob?.audit?.creator}</span>
- </div>
- <div className='flex justify-between'>
- <span className='text-sm'>Created At: </span>
- <span
className='text-sm'>{formatToDateTime(currentJob?.audit?.createTime)}</span>
- </div>
- <div className='flex justify-between'>
- <span className='text-sm'>Updated At: </span>
- <span
className='text-sm'>{formatToDateTime(currentJob?.audit?.lastModifiedTime)}</span>
- </div>
- </div>
- </>
+ <Title level={5} className='mb-2'>
+ Basic Information
+ </Title>
+ <Descriptions column={1} bordered size='small'>
+ <Descriptions.Item label='Job
ID'>{currentJob?.jobId}</Descriptions.Item>
+ <Descriptions.Item label='Template
Name'>{currentJob?.jobTemplateName}</Descriptions.Item>
+ <Descriptions.Item label='Status'>
+ <Tag color={getStatusColor(currentJob?.status ||
'')}>{currentJob?.status}</Tag>
+ </Descriptions.Item>
+ <Descriptions.Item label='Creator'>{currentJob?.audit?.creator
|| '-'}</Descriptions.Item>
+ <Descriptions.Item label='Created At'>
+ {currentJob?.audit?.createTime ?
formatToDateTime(currentJob?.audit?.createTime) : '-'}
+ </Descriptions.Item>
+ <Descriptions.Item label='Updated At'>
+ {currentJob?.audit?.lastModifiedTime ?
formatToDateTime(currentJob?.audit?.lastModifiedTime) : '-'}
+ </Descriptions.Item>
+ </Descriptions>
</Drawer>
)}
{open && (
diff --git a/web-v2/web/src/components/EntityPropertiesFormItem.js
b/web-v2/web/src/components/EntityPropertiesFormItem.js
index a2fcf16826..8fec52025a 100644
--- a/web-v2/web/src/components/EntityPropertiesFormItem.js
+++ b/web-v2/web/src/components/EntityPropertiesFormItem.js
@@ -34,6 +34,7 @@ export default function RenderPropertiesFormItem({ ...props
}) {
const { fields, subOpt, form, isEdit, isDisable, provider, selectBefore } =
props
const disableTableLevelPro = (provider && getPropInfo(provider).immutable)
|| []
const reservedPro = provider ? getPropInfo(provider).reserved : []
+ const allowDeletePro = provider ? getPropInfo(provider).allowDelete : true
const auth = useAppSelector(state => state.auth)
const { systemConfig } = auth
@@ -106,7 +107,8 @@ export default function RenderPropertiesFormItem({ ...props
}) {
}
const handlePropertiesKey = (e, index) => {
- if (isEdit) return
+ const isFieldEdit = !!form.getFieldValue(['properties', index, 'isEdit'])
+ if (isFieldEdit) return
const key = e.target.value
const locationProviders =
@@ -131,155 +133,159 @@ export default function RenderPropertiesFormItem({
...props }) {
return (
<div className='flex flex-col gap-2'>
- {fields.map(subField => (
- <Form.Item label={null} className='align-items-center mb-1'
key={subField.key}>
- <Flex gap='small' align='start'>
- <Form.Item
- name={[subField.name, 'key']}
- className='mb-0 w-full grow'
- label=''
- data-refer={`props-key-${subField.name}`}
- data-testid={`props-key-${subField.name}`}
- rules={[
- {
- pattern: new RegExp(keyRegex),
- message: mismatchForKey
- },
- ({ getFieldValue }) => ({
- validator(_, key) {
- if (getFieldValue('properties').length) {
- if (key) {
- let keys = getFieldValue('properties').map(p => p?.key)
- keys.splice(subField.name, 1)
- if (keys.includes(key)) {
- return Promise.reject(new Error('The key already
exists!'))
- }
- if (reservedPro.includes(key) && !isEdit) {
- return Promise.reject(new Error('The key is
reserved!'))
- }
- if (defaultPro[key]) {
- const { value, select, disabled, description } =
defaultPro[key]
- form.setFieldValue(['properties', subField.name,
'select'], select)
- form.setFieldValue(['properties', subField.name,
'description'], description)
- !form.getFieldValue(['properties', subField.name,
'value']) &&
- value &&
- form.setFieldValue(['properties', subField.name,
'value'], value)
- form.setFieldValue(['properties', subField.name,
'disabled'], disabled)
- } else {
- form.setFieldValue(['properties', subField.name,
'select'], undefined)
- form.setFieldValue(['properties', subField.name,
'description'], '')
- form.setFieldValue(['properties', subField.name,
'disabled'], false)
- }
+ {fields.map(subField => {
+ const isFieldEdit = !!form.getFieldValue(['properties', subField.name,
'isEdit'])
- return Promise.resolve()
- } else {
- return Promise.reject(new Error('The key is
required!'))
- }
- }
- }
- })
- ]}
- >
- <Input
- placeholder={'Key'}
- disabled={
- ([...reservedPro, ...disableTableLevelPro].includes(
- form.getFieldValue(['properties', subField.name, 'key'])
- ) &&
- isEdit) ||
- isDisable
- }
- onChange={e => handlePropertiesKey(e, subField.name)}
- />
- </Form.Item>
- <span className='w-full'>
+ return (
+ <Form.Item label={null} className='align-items-center mb-1'
key={subField.key}>
+ <Flex gap='small' align='start'>
<Form.Item
- name={[subField.name, 'value']}
+ name={[subField.name, 'key']}
className='mb-0 w-full grow'
label=''
- data-refer={`props-value-${subField.name}`}
- data-testid={`props-value-${subField.name}`}
- >
- {form.getFieldValue(['properties', subField.name, 'select']) ?
(
- <Select
- className='flex-none'
- mode={form.getFieldValue(['properties', subField.name,
'multiple']) ? 'multiple' : undefined}
- disabled={form.getFieldValue(['properties', subField.name,
'disabled'])}
- placeholder={
- form.getFieldValue(['properties', subField.name,
'description'])
- ? `${form.getFieldValue(['properties', subField.name,
'description'])}`
- : 'Value'
- }
- >
- {form.getFieldValue(['properties', subField.name,
'select']).map(item => (
- <Select.Option value={item} key={item}>
- {item}
- </Select.Option>
- ))}
- </Select>
- ) : (
- <Input
- placeholder={
- form.getFieldValue(['properties', subField.name,
'description'])
- ? `${form.getFieldValue(['properties', subField.name,
'description'])}`
- : 'Value'
- }
- disabled={
- ([...reservedPro, ...disableTableLevelPro].includes(
- form.getFieldValue(['properties', subField.name,
'key'])
- ) &&
- isEdit) ||
- isDisable ||
- form.getFieldValue(['properties', subField.name,
'disabled'])
- }
- type={
- form.getFieldValue(['properties', subField.name,
'key'])?.includes('password')
- ? 'password'
- : 'text'
- }
- addonBefore={
- form.getFieldValue(['properties', subField.name,
'selectBefore']) ? (
- <Select
- value={form.getFieldValue(['properties',
subField.name, 'prefix'])}
- key={form.getFieldValue(['properties',
subField.name, 'selectBefore']).join(',')}
- onChange={value => form.setFieldValue(['properties',
subField.name, 'prefix'], value)}
- >
- {form.getFieldValue(['properties', subField.name,
'selectBefore']).map(item => (
- <Select.Option key={item} value={item}>
- {item}
- </Select.Option>
- ))}
- </Select>
- ) : null
+ data-refer={`props-key-${subField.name}`}
+ data-testid={`props-key-${subField.name}`}
+ rules={[
+ {
+ pattern: new RegExp(keyRegex),
+ message: mismatchForKey
+ },
+ ({ getFieldValue }) => ({
+ validator(_, key) {
+ if (getFieldValue('properties').length) {
+ if (key) {
+ let keys = getFieldValue('properties').map(p =>
p?.key)
+ keys.splice(subField.name, 1)
+ if (keys.includes(key)) {
+ return Promise.reject(new Error('The key already
exists!'))
+ }
+ if (reservedPro.includes(key) && !isFieldEdit) {
+ return Promise.reject(new Error('The key is
reserved!'))
+ }
+ if (defaultPro[key]) {
+ const { value, select, disabled, description } =
defaultPro[key]
+ form.setFieldValue(['properties', subField.name,
'select'], select)
+ form.setFieldValue(['properties', subField.name,
'description'], description)
+ !form.getFieldValue(['properties', subField.name,
'value']) &&
+ value &&
+ form.setFieldValue(['properties', subField.name,
'value'], value)
+ form.setFieldValue(['properties', subField.name,
'disabled'], disabled)
+ } else {
+ form.setFieldValue(['properties', subField.name,
'select'], undefined)
+ form.setFieldValue(['properties', subField.name,
'description'], '')
+ form.setFieldValue(['properties', subField.name,
'disabled'], false)
+ }
+
+ return Promise.resolve()
+ } else {
+ return Promise.reject(new Error('The key is
required!'))
+ }
+ }
}
- />
- )}
+ })
+ ]}
+ >
+ <Input
+ placeholder={'Key'}
+ disabled={
+ ([...reservedPro, ...disableTableLevelPro].includes(
+ form.getFieldValue(['properties', subField.name, 'key'])
+ ) &&
+ isFieldEdit) ||
+ isDisable
+ }
+ onChange={e => handlePropertiesKey(e, subField.name)}
+ />
</Form.Item>
- </span>
- <Icons.Minus
- className={cn('size-8 cursor-pointer text-gray-400
hover:text-defaultPrimary', {
- 'text-gray-100 hover:text-gray-200 cursor-not-allowed':
- ([...reservedPro, ...disableTableLevelPro].includes(
- form.getFieldValue(['properties', subField.name, 'key'])
- ) &&
- isEdit) ||
- isDisable
- })}
- onClick={() => {
- if (
- ([...reservedPro, ...disableTableLevelPro].includes(
- form.getFieldValue(['properties', subField.name, 'key'])
- ) &&
- isEdit) ||
- isDisable
- )
- return
- subOpt.remove(subField.name)
- }}
- />
- </Flex>
- </Form.Item>
- ))}
+ <span className='w-full'>
+ <Form.Item
+ name={[subField.name, 'value']}
+ className='mb-0 w-full grow'
+ label=''
+ data-refer={`props-value-${subField.name}`}
+ data-testid={`props-value-${subField.name}`}
+ >
+ {form.getFieldValue(['properties', subField.name, 'select'])
? (
+ <Select
+ className='flex-none'
+ mode={form.getFieldValue(['properties', subField.name,
'multiple']) ? 'multiple' : undefined}
+ disabled={form.getFieldValue(['properties',
subField.name, 'disabled'])}
+ placeholder={
+ form.getFieldValue(['properties', subField.name,
'description'])
+ ? `${form.getFieldValue(['properties',
subField.name, 'description'])}`
+ : 'Value'
+ }
+ >
+ {form.getFieldValue(['properties', subField.name,
'select']).map(item => (
+ <Select.Option value={item} key={item}>
+ {item}
+ </Select.Option>
+ ))}
+ </Select>
+ ) : (
+ <Input
+ placeholder={
+ form.getFieldValue(['properties', subField.name,
'description'])
+ ? `${form.getFieldValue(['properties',
subField.name, 'description'])}`
+ : 'Value'
+ }
+ disabled={
+ ([...reservedPro, ...disableTableLevelPro].includes(
+ form.getFieldValue(['properties', subField.name,
'key'])
+ ) &&
+ isFieldEdit) ||
+ isDisable ||
+ form.getFieldValue(['properties', subField.name,
'disabled'])
+ }
+ type={
+ form.getFieldValue(['properties', subField.name,
'key'])?.includes('password')
+ ? 'password'
+ : 'text'
+ }
+ addonBefore={
+ form.getFieldValue(['properties', subField.name,
'selectBefore']) ? (
+ <Select
+ value={form.getFieldValue(['properties',
subField.name, 'prefix'])}
+ key={form.getFieldValue(['properties',
subField.name, 'selectBefore']).join(',')}
+ onChange={value =>
form.setFieldValue(['properties', subField.name, 'prefix'], value)}
+ >
+ {form.getFieldValue(['properties', subField.name,
'selectBefore']).map(item => (
+ <Select.Option key={item} value={item}>
+ {item}
+ </Select.Option>
+ ))}
+ </Select>
+ ) : null
+ }
+ />
+ )}
+ </Form.Item>
+ </span>
+ <Icons.Minus
+ className={cn('size-8 cursor-pointer text-gray-400
hover:text-defaultPrimary', {
+ 'text-gray-100 hover:text-gray-200 cursor-not-allowed':
+ ([...reservedPro, ...disableTableLevelPro].includes(
+ form.getFieldValue(['properties', subField.name, 'key'])
+ ) &&
+ isFieldEdit) ||
+ isDisable
+ })}
+ onClick={() => {
+ if (
+ ([...reservedPro, ...disableTableLevelPro].includes(
+ form.getFieldValue(['properties', subField.name, 'key'])
+ ) &&
+ isFieldEdit) ||
+ isDisable
+ )
+ return
+ subOpt.remove(subField.name)
+ }}
+ />
+ </Flex>
+ </Form.Item>
+ )
+ })}
{rangerAuthType && !isEdit ? (
<Dropdown.Button
data-refer='add-props'