This is an automated email from the ASF dual-hosted git repository.
lauraxia pushed a commit to branch antdUI-gravitino-base1.1.0
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/antdUI-gravitino-base1.1.0 by
this push:
new eb647a6542
[#9684][#9690][#9697][#9698][#9699][#9721][#9722][#9723][#9724][#9725][#9726][#9728][#9729]
eb647a6542 is described below
commit eb647a6542a27f770e0b2a5969f3b0eff7cbe469
Author: Qian Xia <[email protected]>
AuthorDate: Thu Jan 15 18:54:19 2026 +0800
[#9684][#9690][#9697][#9698][#9699][#9721][#9722][#9723][#9724][#9725][#9726][#9728][#9729]
---
web/web/src/app/access/roles/CreateRoleDialog.js | 68 +++-
.../app/access/userGroups/AddUserGroupDialog.js | 1 +
.../userGroups/GrantRolesForUserGroupDialog.js | 2 +-
web/web/src/app/access/users/AddUserDialog.js | 1 +
.../catalogs/rightContent/CreateCatalogDialog.js | 2 +
.../catalogs/rightContent/CreateFilesetDialog.js | 1 +
.../catalogs/rightContent/CreateSchemaDialog.js | 1 +
.../app/catalogs/rightContent/CreateTableDialog.js | 10 +-
.../app/catalogs/rightContent/CreateTopicDialog.js | 1 +
.../app/catalogs/rightContent/LinkVersionDialog.js | 93 ++++--
.../catalogs/rightContent/RegisterModelDialog.js | 1 +
.../entitiesContent/ModelDetailsPage.js | 2 +-
.../app/compliance/policies/CreatePolicyDialog.js | 1 +
web/web/src/app/compliance/tags/CreateTagDialog.js | 1 +
web/web/src/app/jobs/CreateJobDialog.js | 361 ++++++++++++++++-----
web/web/src/app/jobs/RegisterJobTemplateDialog.js | 25 +-
web/web/src/app/metalakes/CreateMetalakeDialog.js | 1 +
web/web/src/app/rootLayout/UserSetting.js | 5 +-
.../src/components/SecurableObjectFormFields.js | 79 ++++-
web/web/src/components/SetOwnerDialog.js | 4 +-
web/web/src/config/index.js | 8 +-
21 files changed, 526 insertions(+), 142 deletions(-)
diff --git a/web/web/src/app/access/roles/CreateRoleDialog.js
b/web/web/src/app/access/roles/CreateRoleDialog.js
index cb40eb365b..f5cc3379e9 100644
--- a/web/web/src/app/access/roles/CreateRoleDialog.js
+++ b/web/web/src/app/access/roles/CreateRoleDialog.js
@@ -47,7 +47,10 @@ export default function CreateRoleDialog({ ...props }) {
const [confirmLoading, setConfirmLoading] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [privilegeErrorTips, setPrivilegeErrorTips] = useState('')
+ const [activeCollapseKeys, setActiveCollapseKeys] = useState([])
const loadedRef = useRef(false)
+ const prevSecurableLenRef = useRef(0)
+ const editInitRef = useRef(false)
const scrollRef = useRef(null)
const scrolling = useScrolling(scrollRef)
const [bottomShadow, setBottomShadow] = useState(false)
@@ -57,6 +60,14 @@ export default function CreateRoleDialog({ ...props }) {
const [form] = Form.useForm()
const values = Form.useWatch([], form)
+ const isElementVisibleInContainer = (el, container) => {
+ if (!el || !container) return true
+ const elRect = el.getBoundingClientRect()
+ const containerRect = container.getBoundingClientRect()
+
+ return elRect.top >= containerRect.top && elRect.bottom <=
containerRect.bottom
+ }
+
const handScroll = () => {
if (scrollRef.current) {
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current
@@ -70,6 +81,18 @@ export default function CreateRoleDialog({ ...props }) {
setTopShadow(false)
setBottomShadow(false)
}
+
+ const activeEl = document.activeElement
+
+ const isFormControl =
+ activeEl && (activeEl.getAttribute?.('role') === 'combobox' ||
['INPUT', 'TEXTAREA'].includes(activeEl.tagName))
+ if (
+ isFormControl &&
+ scrollRef.current.contains(activeEl) &&
+ !isElementVisibleInContainer(activeEl, scrollRef.current)
+ ) {
+ activeEl.blur()
+ }
}
}
@@ -85,6 +108,7 @@ export default function CreateRoleDialog({ ...props }) {
useEffect(() => {
if (open && editRole && !loadedRef.current) {
loadedRef.current = true
+ editInitRef.current = true
const init = async () => {
setIsLoading(true)
@@ -119,6 +143,7 @@ export default function CreateRoleDialog({ ...props }) {
// normal side-effects now.
setTimeout(() => {
form.setFieldsValue({ __init_in_progress: false })
+ editInitRef.current = false
}, 100)
setIsLoading(false)
} catch (e) {
@@ -140,6 +165,33 @@ export default function CreateRoleDialog({ ...props }) {
setPrivilegeErrorTips('')
}, [values?.securableObjects])
+ useEffect(() => {
+ const currentLen = values?.securableObjects?.length || 0
+
+ if (currentLen === 0) {
+ setActiveCollapseKeys([])
+ prevSecurableLenRef.current = 0
+
+ return
+ }
+
+ if (prevSecurableLenRef.current === 0 && activeCollapseKeys.length === 0) {
+ if (editRole) {
+ setActiveCollapseKeys(['0'])
+ } else {
+ setActiveCollapseKeys(Array.from({ length: currentLen }, (_, idx) =>
String(idx)))
+ }
+ } else if (currentLen > prevSecurableLenRef.current) {
+ if (!editRole || !editInitRef.current) {
+ setActiveCollapseKeys(keys => Array.from(new Set([...(keys || []),
String(currentLen - 1)])))
+ }
+ } else if (currentLen < prevSecurableLenRef.current) {
+ setActiveCollapseKeys(keys => (keys || []).filter(k => Number(k) <
currentLen))
+ }
+
+ prevSecurableLenRef.current = currentLen
+ }, [values?.securableObjects?.length, editRole])
+
const handleSubmit = e => {
e.preventDefault()
form
@@ -208,11 +260,9 @@ export default function CreateRoleDialog({ ...props }) {
}
const renderSecurableObjectItems = (fields, subOpt) => {
- const activeKeys = fields.map(f => String(f.key))
-
const items = fields.map(field => {
const fname = field.name
- const fkey = String(field.key)
+ const fkey = String(fname)
// Lightweight internal check: avoid emitting debug logs in production
const titleValue = form.getFieldValue(['securableObjects', fname,
'fullName'])
@@ -247,7 +297,16 @@ export default function CreateRoleDialog({ ...props }) {
return (
<>
- <Collapse defaultActiveKey={activeKeys} accordion={false}
items={items} onChange={handleChangeCollapse} />
+ <Collapse
+ activeKey={activeCollapseKeys}
+ accordion={false}
+ items={items}
+ onChange={keys => {
+ const nextKeys = Array.isArray(keys) ? keys : [keys]
+ setActiveCollapseKeys(nextKeys)
+ handleChangeCollapse()
+ }}
+ />
<div className='text-center mt-2'>
<Button
@@ -301,6 +360,7 @@ export default function CreateRoleDialog({ ...props }) {
name='name'
label='Role Name'
rules={[{ required: true }, { type: 'string', max: 64 }, {
pattern: new RegExp(nameRegex) }]}
+ messageVariables={{ label: 'role name' }}
>
<Input placeholder={mismatchName} disabled={!!editRole} />
</Form.Item>
diff --git a/web/web/src/app/access/userGroups/AddUserGroupDialog.js
b/web/web/src/app/access/userGroups/AddUserGroupDialog.js
index e16a311e9e..c10a2efb11 100644
--- a/web/web/src/app/access/userGroups/AddUserGroupDialog.js
+++ b/web/web/src/app/access/userGroups/AddUserGroupDialog.js
@@ -94,6 +94,7 @@ export default function AddUserGroupDialog({ ...props }) {
name='name'
label='User Group Name'
rules={[{ required: true }, { type: 'string', max: 64 }, {
pattern: new RegExp(nameRegex) }]}
+ messageVariables={{ label: 'user group name' }}
>
<Input placeholder={mismatchName} />
</Form.Item>
diff --git a/web/web/src/app/access/userGroups/GrantRolesForUserGroupDialog.js
b/web/web/src/app/access/userGroups/GrantRolesForUserGroupDialog.js
index e2ec0b45ee..70e1dc610e 100644
--- a/web/web/src/app/access/userGroups/GrantRolesForUserGroupDialog.js
+++ b/web/web/src/app/access/userGroups/GrantRolesForUserGroupDialog.js
@@ -23,7 +23,7 @@ import { validateMessages } from '@/config'
import { useResetFormOnCloseModal } from '@/lib/hooks/use-reset'
import { useAppSelector, useAppDispatch } from '@/lib/hooks/useStore'
import { fetchRoles } from '@/lib/store/roles'
-import { fetchUserGroups, grantRolesForUserGroup } from
'@/lib/store/userGroups'
+import { grantRolesForUserGroup, revokeRolesForUserGroup } from
'@/lib/store/userGroups'
const { Paragraph } = Typography
const { Option } = Select
diff --git a/web/web/src/app/access/users/AddUserDialog.js
b/web/web/src/app/access/users/AddUserDialog.js
index 197974a347..766410d321 100644
--- a/web/web/src/app/access/users/AddUserDialog.js
+++ b/web/web/src/app/access/users/AddUserDialog.js
@@ -99,6 +99,7 @@ export default function AddUserDialog({ ...props }) {
{ type: 'string', max: 64 },
{ pattern: new RegExp(usernameRegex), message: mismatchName }
]}
+ messageVariables={{ label: 'name' }}
>
<Input placeholder={mismatchName} />
</Form.Item>
diff --git a/web/web/src/app/catalogs/rightContent/CreateCatalogDialog.js
b/web/web/src/app/catalogs/rightContent/CreateCatalogDialog.js
index 2cac92bd8a..6dad082668 100644
--- a/web/web/src/app/catalogs/rightContent/CreateCatalogDialog.js
+++ b/web/web/src/app/catalogs/rightContent/CreateCatalogDialog.js
@@ -489,6 +489,7 @@ export default function CreateCatalogDialog({ ...props }) {
name='name'
label='Catalog Name'
rules={[{ required: true }, { type: 'string', max: 64 },
{ pattern: new RegExp(nameRegex) }]}
+ messageVariables={{ label: 'catalog name' }}
data-refer='catalog-name-field'
>
<Input placeholder={mismatchName} disabled={!init} />
@@ -502,6 +503,7 @@ export default function CreateCatalogDialog({ ...props }) {
label={prop.label}
key={idx}
rules={[{ required: prop.required }]}
+ messageVariables={{ label:
prop.label.toLowerCase() }}
initialValue={prop.value}
>
{prop.select ? (
diff --git a/web/web/src/app/catalogs/rightContent/CreateFilesetDialog.js
b/web/web/src/app/catalogs/rightContent/CreateFilesetDialog.js
index fe5a63fee6..da357f5ed3 100644
--- a/web/web/src/app/catalogs/rightContent/CreateFilesetDialog.js
+++ b/web/web/src/app/catalogs/rightContent/CreateFilesetDialog.js
@@ -246,6 +246,7 @@ export default function CreateFilesetDialog({ ...props }) {
name='name'
label='Fileset Name'
rules={[{ required: true }, { type: 'string', max: 64 }, {
pattern: new RegExp(nameRegex) }]}
+ messageVariables={{ label: 'fileset name' }}
>
<Input data-refer='fileset-name-field'
placeholder={mismatchName} disabled={init} />
</Form.Item>
diff --git a/web/web/src/app/catalogs/rightContent/CreateSchemaDialog.js
b/web/web/src/app/catalogs/rightContent/CreateSchemaDialog.js
index 2e290a5106..d9910e3b88 100644
--- a/web/web/src/app/catalogs/rightContent/CreateSchemaDialog.js
+++ b/web/web/src/app/catalogs/rightContent/CreateSchemaDialog.js
@@ -215,6 +215,7 @@ export default function CreateSchemaDialog({ ...props }) {
label='Schema Name'
data-refer='schema-name-field'
rules={[{ required: true }, { type: 'string', max: 64 }, {
pattern: new RegExp(nameRegex) }]}
+ messageVariables={{ label: 'schema name' }}
>
<Input placeholder={mismatchName} disabled={editSchema} />
</Form.Item>
diff --git a/web/web/src/app/catalogs/rightContent/CreateTableDialog.js
b/web/web/src/app/catalogs/rightContent/CreateTableDialog.js
index dc34685399..26c34e8ee0 100644
--- a/web/web/src/app/catalogs/rightContent/CreateTableDialog.js
+++ b/web/web/src/app/catalogs/rightContent/CreateTableDialog.js
@@ -72,6 +72,7 @@ import { capitalizeFirstLetter, genUpdates } from
'@/lib/utils'
import { cn } from '@/lib/utils/tailwind'
import { createTable, updateTable, getTableDetails } from
'@/lib/store/metalakes'
import { useAppDispatch } from '@/lib/hooks/useStore'
+import { includes } from 'lodash-es'
const { Paragraph } = Typography
const { TextArea } = Input
@@ -585,7 +586,7 @@ export default function CreateTableDialog({ ...props }) {
column['defaultValue'] = {
type: 'literal',
dataType: col.defaultValue?.dataType || 'string',
- value: col.defaultValue?.value
+ value: ['', 'NULL',
undefined].includes(col.defaultValue?.value) ? 'NULL' : col.defaultValue?.value
}
}
}
@@ -1299,11 +1300,8 @@ export default function CreateTableDialog({ ...props }) {
<Form.Item
name='name'
label='Table Name'
- rules={[
- { required: true, message: `Please enter the table name!`
},
- { type: 'string', max: 64 },
- { pattern: new RegExp(nameRegex) }
- ]}
+ messageVariables={{ label: 'table name' }}
+ rules={[{ required: true }, { type: 'string', max: 64 }, {
pattern: new RegExp(nameRegex) }]}
>
<Input data-refer='table-name-field'
placeholder={mismatchName} disabled={init} />
</Form.Item>
diff --git a/web/web/src/app/catalogs/rightContent/CreateTopicDialog.js
b/web/web/src/app/catalogs/rightContent/CreateTopicDialog.js
index 4a2646ea5c..c9c7fe962f 100644
--- a/web/web/src/app/catalogs/rightContent/CreateTopicDialog.js
+++ b/web/web/src/app/catalogs/rightContent/CreateTopicDialog.js
@@ -187,6 +187,7 @@ export default function CreateTopicDialog({ ...props }) {
name='name'
label='Topic Name'
rules={[{ required: true }, { type: 'string', max: 64 }, {
pattern: new RegExp(nameRegex) }]}
+ messageVariables={{ label: 'topic name' }}
>
<Input data-refer='topic-name-field'
placeholder={mismatchName} disabled={!!editTopic} />
</Form.Item>
diff --git a/web/web/src/app/catalogs/rightContent/LinkVersionDialog.js
b/web/web/src/app/catalogs/rightContent/LinkVersionDialog.js
index 3231fe6fe2..472444704e 100644
--- a/web/web/src/app/catalogs/rightContent/LinkVersionDialog.js
+++ b/web/web/src/app/catalogs/rightContent/LinkVersionDialog.js
@@ -55,6 +55,7 @@ export default function LinkVersionDialog({ ...props }) {
const [form] = Form.useForm()
const values = Form.useWatch([], form)
const urisItems = Form.useWatch(['uris'], form)
+ const aliasesItems = Form.useWatch(['aliases'], form)
const defaultUriName = Form.useWatch(['properties'], form)?.filter(item =>
item?.key === 'default-uri-name')[0]
const dispatch = useAppDispatch()
@@ -235,7 +236,7 @@ export default function LinkVersionDialog({ ...props }) {
<div className='flex flex-col gap-2'>
{fields.map(({ key, name, ...restField }) => (
<Form.Item label={null}
className='align-items-center mb-1' key={key}>
- <Flex gap='small' align='start'>
+ <Flex gap='small' align='center'>
<Form.Item
className='mb-0 w-full grow'
name={[name, 'name']}
@@ -281,21 +282,36 @@ export default function LinkVersionDialog({ ...props }) {
</Tooltip>
</Form.Item>
)}
- {name === 0 ? (
- <Icons.Plus
- className='size-8 cursor-pointer
text-gray-400 hover:text-defaultPrimary'
- onClick={() => {
- subOpt.add({ name: '', uri: '',
defaultUri: false })
- }}
- />
- ) : (
- <Icons.Minus
- className='size-8 cursor-pointer
text-gray-400 hover:text-defaultPrimary'
- onClick={() => {
- subOpt.remove(name)
- }}
- />
- )}
+ <div className='flex w-10 shrink-0 items-center
justify-start gap-2'>
+ {name === 0 ? (
+ <>
+ <Icons.Plus
+ className='size-4 cursor-pointer
text-gray-400 hover:text-defaultPrimary'
+ onClick={() => {
+ subOpt.add({ name: '', uri: '',
defaultUri: false })
+ }}
+ />
+ {urisItems?.length > 1 && (
+ <Icons.Minus
+ className='size-4 cursor-pointer
text-gray-400 hover:text-defaultPrimary'
+ onClick={() => {
+ subOpt.remove(name)
+ }}
+ />
+ )}
+ </>
+ ) : (
+ <>
+ <span className='inline-block size-4' />
+ <Icons.Minus
+ className='size-4 cursor-pointer
text-gray-400 hover:text-defaultPrimary'
+ onClick={() => {
+ subOpt.remove(name)
+ }}
+ />
+ </>
+ )}
+ </div>
</Flex>
</Form.Item>
))}
@@ -309,7 +325,7 @@ export default function LinkVersionDialog({ ...props }) {
<div className='flex flex-col gap-2'>
{fields.map(({ key, name, ...restField }) => (
<Form.Item label={null}
className='align-items-center mb-1' key={key}>
- <div className='flex items-start gap-2'>
+ <Flex gap='small' align='center'>
<Form.Item
{...restField}
className='mb-0 w-full grow'
@@ -339,24 +355,37 @@ export default function LinkVersionDialog({ ...props }) {
>
<Input placeholder={`Alias ${name + 1}`} />
</Form.Item>
- <Form.Item className='mb-0 grow-0'>
+ <div className='flex w-10 shrink-0 items-center
justify-start gap-2'>
{name === 0 ? (
- <Icons.Plus
- className='size-4 cursor-pointer
text-gray-400 hover:text-defaultPrimary'
- onClick={() => {
- subOpt.add()
- }}
- />
+ <>
+ <Icons.Plus
+ className='size-4 cursor-pointer
text-gray-400 hover:text-defaultPrimary'
+ onClick={() => {
+ subOpt.add()
+ }}
+ />
+ {aliasesItems?.length > 1 && (
+ <Icons.Minus
+ className='size-4 cursor-pointer
text-gray-400 hover:text-defaultPrimary'
+ onClick={() => {
+ subOpt.remove(name)
+ }}
+ />
+ )}
+ </>
) : (
- <Icons.Minus
- className='size-4 cursor-pointer
text-gray-400 hover:text-defaultPrimary'
- onClick={() => {
- subOpt.remove(name)
- }}
- />
+ <>
+ <span className='inline-block size-4' />
+ <Icons.Minus
+ className='size-4 cursor-pointer
text-gray-400 hover:text-defaultPrimary'
+ onClick={() => {
+ subOpt.remove(name)
+ }}
+ />
+ </>
)}
- </Form.Item>
- </div>
+ </div>
+ </Flex>
</Form.Item>
))}
</div>
diff --git a/web/web/src/app/catalogs/rightContent/RegisterModelDialog.js
b/web/web/src/app/catalogs/rightContent/RegisterModelDialog.js
index 0fe81620f9..5c6994d22b 100644
--- a/web/web/src/app/catalogs/rightContent/RegisterModelDialog.js
+++ b/web/web/src/app/catalogs/rightContent/RegisterModelDialog.js
@@ -190,6 +190,7 @@ export default function RegisterModelDialog({ ...props }) {
name='name'
label='Model Name'
rules={[{ required: true }, { type: 'string', max: 64 }, {
pattern: new RegExp(nameRegex) }]}
+ messageVariables={{ label: 'model name' }}
>
<Input placeholder={mismatchName} disabled={init} />
</Form.Item>
diff --git
a/web/web/src/app/catalogs/rightContent/entitiesContent/ModelDetailsPage.js
b/web/web/src/app/catalogs/rightContent/entitiesContent/ModelDetailsPage.js
index ec777e2cad..b57351f3d1 100644
--- a/web/web/src/app/catalogs/rightContent/entitiesContent/ModelDetailsPage.js
+++ b/web/web/src/app/catalogs/rightContent/entitiesContent/ModelDetailsPage.js
@@ -275,7 +275,7 @@ export default function ModelDetailsPage({ ...props }) {
}
}
],
- [currentMetalake]
+ [currentMetalake, catalog, schema, model]
)
const { resizableColumns, components, tableWidth } = useAntdColumnResize(()
=> {
diff --git a/web/web/src/app/compliance/policies/CreatePolicyDialog.js
b/web/web/src/app/compliance/policies/CreatePolicyDialog.js
index c7098aef21..5787979e20 100644
--- a/web/web/src/app/compliance/policies/CreatePolicyDialog.js
+++ b/web/web/src/app/compliance/policies/CreatePolicyDialog.js
@@ -170,6 +170,7 @@ export default function CreatePolicyDialog({ ...props }) {
name='name'
label='Policy Name'
rules={[{ required: true }, { type: 'string', max: 64 }, {
pattern: new RegExp(nameRegex) }]}
+ messageVariables={{ label: 'policy name' }}
>
<Input placeholder={mismatchName} />
</Form.Item>
diff --git a/web/web/src/app/compliance/tags/CreateTagDialog.js
b/web/web/src/app/compliance/tags/CreateTagDialog.js
index d85b8493c2..5cd1003016 100644
--- a/web/web/src/app/compliance/tags/CreateTagDialog.js
+++ b/web/web/src/app/compliance/tags/CreateTagDialog.js
@@ -153,6 +153,7 @@ export default function CreateTagDialog({ ...props }) {
name='name'
label='Tag Name'
rules={[{ required: true }, { type: 'string', max: 64 }, {
pattern: new RegExp(nameRegex) }]}
+ messageVariables={{ label: 'tag name' }}
>
<Input placeholder={mismatchName} />
</Form.Item>
diff --git a/web/web/src/app/jobs/CreateJobDialog.js
b/web/web/src/app/jobs/CreateJobDialog.js
index 313206ad1f..31b818889a 100644
--- a/web/web/src/app/jobs/CreateJobDialog.js
+++ b/web/web/src/app/jobs/CreateJobDialog.js
@@ -41,6 +41,7 @@ export default function CreateJobDialog({ ...props }) {
const { open, setOpen, metalake, router, jobTemplateNames } = props
const [confirmLoading, setConfirmLoading] = useState(false)
const [isLoading, setIsLoading] = useState(false)
+ const [jobTemplateDetail, setJobTemplateDetail] = useState(null)
const [form] = Form.useForm()
const values = Form.useWatch([], form)
const currentJobTemplate = Form.useWatch('jobTemplateName', form)
@@ -64,24 +65,80 @@ export default function CreateJobDialog({ ...props }) {
}, [open, jobTemplateNames, currentJobTemplate])
const updateOnChange = (index, newValue) => {
- const changedTemplate = jobConfValues[index].template
- if (!changedTemplate) return
+ const changedKey = jobConfValues?.[index]?.confKey
+ if (!changedKey) return
- const updatedJobConf = [...jobConfValues]
+ const updatedJobConf = (jobConfValues || []).map(conf =>
+ conf?.confKey === changedKey ? { ...conf, value: newValue } : conf
+ )
- jobConfValues.forEach(index => {
- let hasChange = false
- updatedJobConf.forEach((conf, i) => {
- if (i !== index && conf?.template === changedTemplate) {
- updatedJobConf[i] = { ...conf, value: newValue }
- hasChange = true
- }
- })
+ form.setFieldsValue({ jobConf: updatedJobConf })
+ }
+
+ const getPlaceholderEntries = templateDetail => {
+ if (!templateDetail) return []
+
+ const jobType = String(templateDetail.jobType || '').toLowerCase()
+ const stringValues = []
+
+ const pushString = value => {
+ if (typeof value === 'string' && value) {
+ stringValues.push(value)
+ }
+ }
+
+ const pushArray = values => {
+ ;(values || []).forEach(val => pushString(val))
+ }
+
+ const pushObjectValues = obj => {
+ Object.values(obj || {}).forEach(val => pushString(val))
+ }
+
+ pushString(templateDetail.jobType)
+ pushString(templateDetail.comment)
+ pushString(templateDetail.executable)
+ pushArray(templateDetail.arguments)
+ pushObjectValues(templateDetail.environments)
+ pushObjectValues(templateDetail.customFields)
+
+ if (jobType === 'spark') {
+ pushString(templateDetail.className)
+ pushArray(templateDetail.jars)
+ pushArray(templateDetail.files)
+ pushArray(templateDetail.archives)
+ pushObjectValues(templateDetail.configs)
+ }
+
+ if (jobType === 'shell') {
+ pushArray(templateDetail.scripts)
+ }
+
+ const placeholders = stringValues.flatMap(template =>
getJobParamValues(template) || []).filter(Boolean)
+ const uniquePlaceholders = Array.from(new Set(placeholders))
+
+ return uniquePlaceholders.map(name => ({
+ key: name,
+ value: '',
+ confKey: name,
+ template: `{{${name}}}`
+ }))
+ }
- if (hasChange) {
- form.setFieldsValue({ jobConf: updatedJobConf })
+ const getPlaceholderValueMap = () =>
+ (jobConfValues || []).reduce((acc, item) => {
+ if (item?.confKey) {
+ acc[item.confKey] = item.value || ''
}
- })
+
+ return acc
+ }, {})
+
+ const renderTemplateValue = template => {
+ if (!template) return ''
+ const valueMap = getPlaceholderValueMap()
+
+ return template.replace(/\{\{([^}]+)\}\}/g, (_, key) => valueMap[key] ||
`{{${key}}}`)
}
useEffect(() => {
@@ -90,32 +147,9 @@ export default function CreateJobDialog({ ...props }) {
try {
const { payload: jobTemplate } = await dispatch(getJobTemplate({
metalake, jobTemplate: currentJobTemplate }))
setIsLoading(false)
- form.setFieldValue('jobConf', [])
- const { arguments: args, environments, customFields } = jobTemplate
- args.forEach((conf, index) => {
- form.setFieldValue(['jobConf', index], {
- key: getJobParamValues(conf)?.[0],
- value: '',
- confKey: getJobParamValues(conf)?.[0],
- template: conf
- })
- })
- Object.entries(environments).forEach(([key, value], index) => {
- form.setFieldValue(['jobConf', args.length + index], {
- key,
- value: '',
- confKey: getJobParamValues(value)?.[0],
- template: value
- })
- })
- Object.entries(customFields).forEach(([key, value], index) => {
- form.setFieldValue(['jobConf', args.length +
Object.keys(environments).length + index], {
- key,
- value: '',
- confKey: getJobParamValues(value)?.[0],
- template: value
- })
- })
+ setJobTemplateDetail(jobTemplate)
+ const placeholderEntries = getPlaceholderEntries(jobTemplate)
+ form.setFieldsValue({ jobConf: placeholderEntries })
} catch (error) {
console.log(error)
setIsLoading(false)
@@ -166,7 +200,7 @@ export default function CreateJobDialog({ ...props }) {
okText='Submit'
maskClosable={false}
keyboard={false}
- width={800}
+ width={1000}
confirmLoading={confirmLoading}
onCancel={handleCancel}
>
@@ -179,7 +213,12 @@ export default function CreateJobDialog({ ...props }) {
name='policyForm'
validateMessages={validateMessages}
>
- <Form.Item name='jobTemplateName' label='Template Name' rules={[{
required: true }]}>
+ <Form.Item
+ name='jobTemplateName'
+ label='Template Name'
+ rules={[{ required: true }]}
+ messageVariables={{ label: 'template name' }}
+ >
<Select
popupRender={menu => (
<>
@@ -210,47 +249,211 @@ export default function CreateJobDialog({ ...props }) {
options={jobTemplateNames.map(template => ({ label: template,
value: template }))}
/>
</Form.Item>
- <Form.Item label='Job Configuration'>
- <div className='flex flex-col gap-2'>
- <Form.List name='jobConf'>
- {(fields, { add, remove }) => (
+ <div className='relative rounded border border-gray-200'>
+ <div className='pointer-events-none absolute inset-y-0 left-1/2
w-px -translate-x-1/2 bg-gray-200' />
+ <div className='pointer-events-none absolute left-1/2 top-1/2
flex size-6 -translate-x-1/2 -translate-y-1/2 items-center justify-center
rounded-full border border-gray-200 bg-white text-gray-500'>
+ <Icons.iconify icon='mdi:arrow-left' className='size-4' />
+ </div>
+ <div className='grid grid-cols-2 gap-6 p-4'>
+ <div className='space-y-2 max-h-[320px] overflow-y-auto pr-2'>
+ <Paragraph className='text-sm !mb-0'>Template
Parameters</Paragraph>
+ <div>
+ <Paragraph className='text-[12px] mb-1'>Basic
Fields</Paragraph>
+ <div className='pl-4 space-y-1'>
+ <div className='text-[12px] text-gray-700'>
+ <span className='text-gray-400'>Job Type:</span>
{jobTemplateDetail?.jobType || '-'}
+ </div>
+ <div className='text-[12px] text-gray-700'>
+ <span className='text-gray-400'>Comment:</span>
{jobTemplateDetail?.comment || '-'}
+ </div>
+ <div className='text-[12px] text-gray-700'>
+ <span className='text-gray-400'>Executable:</span>{' '}
+ {renderTemplateValue(jobTemplateDetail?.executable ||
'') || '-'}
+ </div>
+ {jobTemplateDetail?.jobType === 'spark' && (
+ <div className='text-[12px] text-gray-700'>
+ <span className='text-gray-400'>Class Name:</span>{'
'}
+ {renderTemplateValue(jobTemplateDetail?.className ||
'') || '-'}
+ </div>
+ )}
+ </div>
+ </div>
+ <div>
+ <Paragraph className='text-[12px]
mb-1'>Arguments</Paragraph>
+ <div className='pl-4'>
+ {(jobTemplateDetail?.arguments || []).length > 0 ? (
+ <div className='space-y-1'>
+ {(jobTemplateDetail?.arguments || []).map((arg,
index) => (
+ <div key={`arg-${index}`} className='text-sm
text-gray-700'>
+ {renderTemplateValue(arg)}
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className='text-sm text-gray-400'>No
arguments</div>
+ )}
+ </div>
+ </div>
+ <div>
+ <Paragraph className='text-[12px] mb-1'>Environment
Variables</Paragraph>
+ <div className='pl-4'>
+ {Object.keys(jobTemplateDetail?.environments ||
{}).length > 0 ? (
+ <div className='space-y-1'>
+ {Object.entries(jobTemplateDetail?.environments ||
{}).map(([key, value]) => (
+ <div key={`env-${key}`} className='text-sm
text-gray-700'>
+ <span className='text-gray-400'>{key}:</span>
{renderTemplateValue(value)}
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className='text-[12px] text-gray-400'>No
environment variables</div>
+ )}
+ </div>
+ </div>
+ <div>
+ <Paragraph className='text-[12px] mb-1'>Custom
Fields</Paragraph>
+ <div className='pl-4'>
+ {Object.keys(jobTemplateDetail?.customFields ||
{}).length > 0 ? (
+ <div className='space-y-1'>
+ {Object.entries(jobTemplateDetail?.customFields ||
{}).map(([key, value]) => (
+ <div key={`custom-${key}`} className='text-sm
text-gray-700'>
+ <span className='text-gray-400'>{key}:</span>
{renderTemplateValue(value)}
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className='text-[12px] text-gray-400'>No custom
fields</div>
+ )}
+ </div>
+ </div>
+ {jobTemplateDetail?.jobType === 'spark' && (
<>
- {fields.map(({ key, name, ...restField }) => (
- <Form.Item label={null} className='align-items-center
mb-1' key={key}>
- <Flex gap='small' align='start' key={key}>
- <Form.Item
- {...restField}
- name={[name, 'key']}
- rules={[{ required: true, message: 'Please enter
the job config key!' }]}
- className='mb-0 w-full grow'
- >
- <Input disabled placeholder='Job Config Key' />
- </Form.Item>
- <Form.Item
- {...restField}
- name={[name, 'value']}
- rules={[{ required: true, message: 'Please enter
the job config value!' }]}
- className='mb-0 w-full grow'
- >
- <Input
- placeholder={form.getFieldValue(['jobConf',
name, 'template']) || 'Job Config Value'}
- onChange={e => updateOnChange(name,
e.target.value)}
- />
- </Form.Item>
- <Form.Item className='mb-0 grow-0'>
- <Icons.Minus
- className='size-8 cursor-pointer text-gray-400
hover:text-defaultPrimary'
- onClick={() => remove(name)}
- />
- </Form.Item>
- </Flex>
- </Form.Item>
- ))}
+ <div>
+ <Paragraph className='text-[12px]
mb-1'>Jars</Paragraph>
+ <div className='pl-4'>
+ {(jobTemplateDetail?.jars || []).length > 0 ? (
+ <div className='space-y-1'>
+ {(jobTemplateDetail?.jars || []).map((item,
index) => (
+ <div key={`jar-${index}`} className='text-sm
text-gray-700'>
+ {renderTemplateValue(item)}
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className='text-[12px] text-gray-400'>No
jars</div>
+ )}
+ </div>
+ </div>
+ <div>
+ <Paragraph className='text-[12px]
mb-1'>Files</Paragraph>
+ <div className='pl-4'>
+ {(jobTemplateDetail?.files || []).length > 0 ? (
+ <div className='space-y-1'>
+ {(jobTemplateDetail?.files || []).map((item,
index) => (
+ <div key={`file-${index}`} className='text-sm
text-gray-700'>
+ {renderTemplateValue(item)}
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className='text-[12px] text-gray-400'>No
files</div>
+ )}
+ </div>
+ </div>
+ <div>
+ <Paragraph className='text-[12px]
mb-1'>Archives</Paragraph>
+ <div className='pl-4'>
+ {(jobTemplateDetail?.archives || []).length > 0 ? (
+ <div className='space-y-1'>
+ {(jobTemplateDetail?.archives || []).map((item,
index) => (
+ <div key={`archive-${index}`}
className='text-sm text-gray-700'>
+ {renderTemplateValue(item)}
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className='text-[12px] text-gray-400'>No
archives</div>
+ )}
+ </div>
+ </div>
+ <div>
+ <Paragraph className='text-[12px]
mb-1'>Configs</Paragraph>
+ <div className='pl-4'>
+ {Object.keys(jobTemplateDetail?.configs ||
{}).length > 0 ? (
+ <div className='space-y-1'>
+ {Object.entries(jobTemplateDetail?.configs ||
{}).map(([key, value]) => (
+ <div key={`config-${key}`}
className='text-[12px] text-gray-700'>
+ <span
className='text-gray-400'>{key}:</span> {renderTemplateValue(value)}
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className='text-[12px] text-gray-400'>No
configs</div>
+ )}
+ </div>
+ </div>
</>
)}
- </Form.List>
+ {jobTemplateDetail?.jobType === 'shell' && (
+ <div>
+ <Paragraph className='text-[12px]
mb-1'>Scripts</Paragraph>
+ <div className='pl-4'>
+ {(jobTemplateDetail?.scripts || []).length > 0 ? (
+ <div className='space-y-1'>
+ {(jobTemplateDetail?.scripts || []).map((item,
index) => (
+ <div key={`script-${index}`} className='text-sm
text-gray-700'>
+ {renderTemplateValue(item)}
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className='text-[12px] text-gray-400'>No
scripts</div>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ <div className='pl-2 max-h-[320px] overflow-y-auto pr-2'>
+ <Form.Item label='Job Configuration'>
+ <div className='flex flex-col gap-2'>
+ <Form.List name='jobConf'>
+ {fields => (
+ <>
+ {fields.map(({ key, name, ...restField }) => (
+ <Form.Item label={null}
className='align-items-center mb-0' key={key}>
+ <Flex gap='small' align='start' key={key}>
+ <Form.Item
+ {...restField}
+ name={[name, 'key']}
+ rules={[{ required: true, message: 'Please
enter the job config key!' }]}
+ className='mb-0 w-full grow'
+ >
+ <Input disabled placeholder='Job Config
Key' />
+ </Form.Item>
+ <Form.Item
+ {...restField}
+ name={[name, 'value']}
+ rules={[{ required: true, message: 'Please
enter the job config value!' }]}
+ className='mb-0 w-full grow'
+ >
+ <Input
+ placeholder={
+ form.getFieldValue(['jobConf', name,
'template']) || 'Job Config Value'
+ }
+ onChange={e => updateOnChange(name,
e.target.value)}
+ />
+ </Form.Item>
+ </Flex>
+ </Form.Item>
+ ))}
+ </>
+ )}
+ </Form.List>
+ </div>
+ </Form.Item>
+ </div>
</div>
- </Form.Item>
+ </div>
</Form>
</Spin>
{openTemplate && (
diff --git a/web/web/src/app/jobs/RegisterJobTemplateDialog.js
b/web/web/src/app/jobs/RegisterJobTemplateDialog.js
index 02b781cc89..9968c3af53 100644
--- a/web/web/src/app/jobs/RegisterJobTemplateDialog.js
+++ b/web/web/src/app/jobs/RegisterJobTemplateDialog.js
@@ -239,6 +239,7 @@ export default function RegisterJobTemplateDialog({
...props }) {
name='name'
label='Template Name'
rules={[{ required: true }, { type: 'string', max: 64 }, {
pattern: new RegExp(nameRegex) }]}
+ messageVariables={{ label: 'template name' }}
>
<Input placeholder={mismatchName} />
</Form.Item>
@@ -256,10 +257,10 @@ export default function RegisterJobTemplateDialog({
...props }) {
rules={[{ required: true, message: 'Please enter the
executable!' }]}
label='Executable'
>
- <Input />
+ <Input placeholder='e.g. /path/to/my_script.sh' />
</Form.Item>
<Form.Item name='arguments' label='Arguments'>
- <Select mode='tags' tokenSeparators={[',']} />
+ <Select mode='tags' tokenSeparators={[',']}
placeholder='e.g. {{arg1}},{{arg2}}' />
</Form.Item>
<Form.Item label='Environment Variables'>
<div className='flex flex-col gap-2'>
@@ -355,16 +356,28 @@ export default function RegisterJobTemplateDialog({
...props }) {
rules={[{ required: true, message: 'Please enter the
class name!' }]}
label='Class Name'
>
- <Input />
+ <Input placeholder='e.g. com.example.MySparkApp' />
</Form.Item>
<Form.Item name='jars' label='Jars'>
- <Select mode='tags' tokenSeparators={[',']} />
+ <Select
+ mode='tags'
+ tokenSeparators={[',']}
+ placeholder='e.g.
/path/to/dependency1.jar,/path/to/dependency2.jar'
+ />
</Form.Item>
<Form.Item name='files' label='Files'>
- <Select mode='tags' tokenSeparators={[',']} />
+ <Select
+ mode='tags'
+ tokenSeparators={[',']}
+ placeholder='e.g.
/path/to/file1.txt,/path/to/file2.txt'
+ />
</Form.Item>
<Form.Item name='archives' label='Archives'>
- <Select mode='tags' tokenSeparators={[',']} />
+ <Select
+ mode='tags'
+ tokenSeparators={[',']}
+ placeholder='e.g.
/path/to/archive1.zip,/path/to/archive2.zip'
+ />
</Form.Item>
<Form.Item label='Configs'>
<div className='flex flex-col gap-2'>
diff --git a/web/web/src/app/metalakes/CreateMetalakeDialog.js
b/web/web/src/app/metalakes/CreateMetalakeDialog.js
index 2905dc6441..20c9e79339 100644
--- a/web/web/src/app/metalakes/CreateMetalakeDialog.js
+++ b/web/web/src/app/metalakes/CreateMetalakeDialog.js
@@ -144,6 +144,7 @@ export default function CreateMetalakeDialog({ ...props }) {
label='Name'
data-refer='metalake-name-field'
rules={[{ required: true }, { type: 'string', max: 64 }, {
pattern: new RegExp(nameRegex) }]}
+ messageVariables={{ label: 'name' }}
>
<Input placeholder={validateMessages.pattern?.mismatch} />
</Form.Item>
diff --git a/web/web/src/app/rootLayout/UserSetting.js
b/web/web/src/app/rootLayout/UserSetting.js
index 42aff8ce04..22b533ea73 100644
--- a/web/web/src/app/rootLayout/UserSetting.js
+++ b/web/web/src/app/rootLayout/UserSetting.js
@@ -26,7 +26,7 @@ import { usePathname, useSearchParams, useRouter } from
'next/navigation'
import { cn } from '@/lib/utils/tailwind'
import { useAppSelector, useAppDispatch } from '@/lib/hooks/useStore'
import Loading from '@/components/Loading'
-import { fetchMetalakes } from '@/lib/store/metalakes'
+import { fetchMetalakes, resetMetalakeStore } from '@/lib/store/metalakes'
import { logoutAction } from '@/lib/store/auth'
import { oauthProviderFactory } from '@/lib/auth/providers/factory'
@@ -110,6 +110,9 @@ export default function UserSetting() {
const params = new URLSearchParams(searchParams.toString())
params.set('metalake', metalake.name)
+ // Reset metalake store to avoid data inconsistency
+ dispatch(resetMetalakeStore())
+
// Preserve pathname and only update metalake in query string
router.push(`${pathname}?${params.toString()}`)
}}
diff --git a/web/web/src/components/SecurableObjectFormFields.js
b/web/web/src/components/SecurableObjectFormFields.js
index 9e7dcad2ad..d754beaca5 100644
--- a/web/web/src/components/SecurableObjectFormFields.js
+++ b/web/web/src/components/SecurableObjectFormFields.js
@@ -20,7 +20,7 @@
import React, { useEffect, useLayoutEffect, useState, useRef } from 'react'
import { Form, Input, Select, Checkbox, Spin } from 'antd'
import { to } from '@/lib/utils'
-import { useAppDispatch } from '@/lib/hooks/useStore'
+import { useAppDispatch, useAppSelector } from '@/lib/hooks/useStore'
import {
fetchCatalogs,
fetchSchemas,
@@ -45,6 +45,7 @@ export default function SecurableObjectFormFields({
fieldName, fieldKey, metalak
const currentFull = String(currentFullRaw || '')
const displayValue = currentFull
const dispatch = useAppDispatch()
+ const store = useAppSelector(state => state.metalakes)
const parts = String(currentFull || '')
.split('.')
@@ -70,6 +71,36 @@ export default function SecurableObjectFormFields({
fieldName, fieldKey, metalak
const [localRemoteOptions, setLocalRemoteOptions] = useState({})
const [localRemoteLoading, setLocalRemoteLoading] = useState({})
const catalogOptions = localRemoteOptions['catalog'] || []
+ const rootRef = useRef(null)
+
+ const isElementVisibleInViewport = el => {
+ if (!el) return true
+ const rect = el.getBoundingClientRect()
+
+ return rect.top >= 0 && rect.bottom <= (window.innerHeight ||
document.documentElement.clientHeight)
+ }
+
+ useEffect(() => {
+ const handleScrollOrResize = () => {
+ const activeEl = document.activeElement
+ if (!activeEl || !rootRef.current) return
+
+ const isFormControl =
+ activeEl.getAttribute?.('role') === 'combobox' || ['INPUT',
'TEXTAREA'].includes(activeEl.tagName)
+
+ if (isFormControl && rootRef.current.contains(activeEl) &&
!isElementVisibleInViewport(activeEl)) {
+ activeEl.blur()
+ }
+ }
+
+ window.addEventListener('scroll', handleScrollOrResize, true)
+ window.addEventListener('resize', handleScrollOrResize, true)
+
+ return () => {
+ window.removeEventListener('scroll', handleScrollOrResize, true)
+ window.removeEventListener('resize', handleScrollOrResize, true)
+ }
+ }, [])
const loadOptionsForType = async type => {
if (!metalake) return
@@ -123,6 +154,30 @@ export default function SecurableObjectFormFields({
fieldName, fieldKey, metalak
}
}
+ const getCatalogTypeForName = async catalogName => {
+ if (!catalogName) return null
+
+ const cachedType =
+ catalogOptions.find(c => c.value === catalogName)?.catalogType ||
+ store.catalogs.find(c => c.value === catalogName)?.catalogType
+ if (cachedType) return cachedType
+
+ const [err, res] = await to(dispatch(fetchCatalogs({ metalake })))
+ if (err || !res) return null
+
+ const catalogs = res?.payload?.catalogs || []
+ const matched = catalogs.find(c => c.name === catalogName)
+
+ if (matched?.type) {
+ setLocalRemoteOptions(prev => ({
+ ...prev,
+ catalog: catalogs.map(c => ({ label: c.name, value: c.name,
catalogType: c.type }))
+ }))
+ }
+
+ return matched?.type || null
+ }
+
const getPrivilegeGroupsForType = async (type, catalogName) => {
if (!type) return []
@@ -147,7 +202,8 @@ export default function SecurableObjectFormFields({
fieldName, fieldKey, metalak
if (schemaGroup) groups.push(schemaGroup)
// If we have a cached catalog type, append the corresponding resource
group
- const catalogType = catalogName ? catalogOptions.filter(c => c.value ===
catalogName)[0]?.catalogType : null
+ const catalogType = await getCatalogTypeForName(catalogName)
+
if (catalogType) {
if (catalogType === 'relational') {
const tableGroup = privilegeOptions.find(g => g.label === 'Table
privileges')
@@ -174,7 +230,7 @@ export default function SecurableObjectFormFields({
fieldName, fieldKey, metalak
if (useSchemaOptions.length > 0) groups.push({ label:
schemaGroup.label, options: useSchemaOptions })
}
- const catalogType = catalogName ? catalogOptions.filter(c => c.value ===
catalogName)[0]?.catalogType : null
+ const catalogType = await getCatalogTypeForName(catalogName)
if (catalogType) {
if (catalogType === 'relational') {
const tableGroup = privilegeOptions.find(g => g.label === 'Table
privileges')
@@ -490,15 +546,26 @@ export default function SecurableObjectFormFields({
fieldName, fieldKey, metalak
const denyAllOptionValues = getAllOptionValues('denyPrivileges')
return (
- <div className='flex flex-col gap-2'>
+ <div className='flex flex-col gap-2' ref={rootRef}>
<div>
- <Form.Item name={[fieldName, 'type']} label='Type' rules={[{ required:
true }]} style={{ marginBottom: 8 }}>
+ <Form.Item
+ name={[fieldName, 'type']}
+ label='Type'
+ rules={[{ required: true }]}
+ messageVariables={{ label: 'type' }}
+ style={{ marginBottom: 8 }}
+ >
<Select placeholder='Please select type' options={privilegeTypes} />
</Form.Item>
</div>
<div>
- <Form.Item name={[fieldName, 'fullName']} label='Full Name' rules={[{
required: true }]}>
+ <Form.Item
+ name={[fieldName, 'fullName']}
+ label='Full Name'
+ rules={[{ required: true }]}
+ messageVariables={{ label: 'full name' }}
+ >
{(() => {
if (currentType === 'metalake') {
return <Input value={metalake} disabled />
diff --git a/web/web/src/components/SetOwnerDialog.js
b/web/web/src/components/SetOwnerDialog.js
index 10aa704b6f..2957c256f9 100644
--- a/web/web/src/components/SetOwnerDialog.js
+++ b/web/web/src/components/SetOwnerDialog.js
@@ -76,7 +76,7 @@ export default function SetOwnerDialog({ ...props }) {
await dispatch(setEntityOwner({ metalake, metadataObjectType,
metadataObjectFullName, data: submitData }))
setConfirmLoading(false)
mutateOwner && mutateOwner()
- setOpen(false)
+ setOpen(false, true)
})
.catch(info => {
console.error(info)
@@ -85,7 +85,7 @@ export default function SetOwnerDialog({ ...props }) {
}
const handleCancel = () => {
- setOpen(false)
+ setOpen(false, false)
}
return (
diff --git a/web/web/src/config/index.js b/web/web/src/config/index.js
index 82d9908400..eed62cf22e 100644
--- a/web/web/src/config/index.js
+++ b/web/web/src/config/index.js
@@ -30,17 +30,17 @@ export const mismatchUsername =
export const mismatchForKey =
'Must start with a letter/underscore(_), contain only alphanumeric
characters (Aa-Zz,0-9) or underscores (_), hyphens(-), or dots(.)!'
-export const validateMessages = () => ({
- required: `Please enter the ${label}!`,
+export const validateMessages = {
+ required: 'Please enter the ${label}!',
string: {
- max: `The ${label} must be less than ${max} characters!`
+ max: 'The ${label} must be less than ${max} characters!'
},
pattern: {
mismatch: mismatchName,
mismatchusername: mismatchUsername,
mismatchforkey: mismatchForKey
}
-})
+}
export const supportedObjectTypesMap = {
CATALOG: 'catalog',