{children}
@@ -14,7 +27,9 @@ const ScrollBox = ({ children, style, ...rest }) => {
ScrollBox.propTypes = {
children: PropTypes.node,
- style: PropTypes.object
+ style: PropTypes.object,
+ horizontalBottomPadding: PropTypes.number,
+ verticalRightPadding: PropTypes.number
}
export default ScrollBox
diff --git a/src/components/Dashboard/common/TagsDisplay.jsx b/src/components/Dashboard/common/TagsDisplay.jsx
index 7e27ee6..87ae800 100644
--- a/src/components/Dashboard/common/TagsDisplay.jsx
+++ b/src/components/Dashboard/common/TagsDisplay.jsx
@@ -1,15 +1,19 @@
-import { Tag, Space, Typography } from 'antd'
+import { Tag, Flex, Typography } from 'antd'
import PropTypes from 'prop-types'
-
const { Text } = Typography
+import { useEffect, useState } from 'react'
+import ScrollBox from './ScrollBox'
-const TagsDisplay = ({ tags, style }) => {
- let tagArray = []
- if (typeof tags === 'string') {
- tagArray = [tags]
- } else if (Array.isArray(tags)) {
- tagArray = tags
- }
+const TagsDisplay = ({ tags, style, scrollHorizontal = false }) => {
+ const [tagArray, setTagArray] = useState([])
+
+ useEffect(() => {
+ if (typeof tags === 'string') {
+ setTagArray([tags])
+ } else if (Array.isArray(tags)) {
+ setTagArray(tags)
+ }
+ }, [tags])
if (
!tagArray ||
@@ -19,20 +23,27 @@ const TagsDisplay = ({ tags, style }) => {
return
n/a
}
- return (
-
+ const tagContents = (
+
{tagArray.map((tag, index) => (
{tag}
))}
-
+
)
+
+ if (scrollHorizontal) {
+ return
{tagContents}
+ } else {
+ return tagContents
+ }
}
TagsDisplay.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string),
- style: PropTypes.object
+ style: PropTypes.object,
+ scrollHorizontal: PropTypes.bool
}
export default TagsDisplay
diff --git a/src/components/Dashboard/common/TagsInput.jsx b/src/components/Dashboard/common/TagsInput.jsx
index 4f50808..7a0be0d 100644
--- a/src/components/Dashboard/common/TagsInput.jsx
+++ b/src/components/Dashboard/common/TagsInput.jsx
@@ -1,56 +1,179 @@
-import { useState } from 'react'
-import { Space, Tag, Input, Button } from 'antd'
+import { useState, useEffect, useContext, useMemo } from 'react'
+import { Space, Button, TreeSelect, Tag } from 'antd'
import PlusIcon from '../../Icons/PlusIcon'
import PropTypes from 'prop-types'
+import { ApiServerContext } from '../context/ApiServerContext'
+import TagsDisplay from './TagsDisplay'
-const TagsInput = ({ value = [], onChange }) => {
- const [inputValue, setInputValue] = useState('')
+const { SHOW_CHILD } = TreeSelect
- const handleTagClose = (removedTag) => {
- const newTags = value.filter((tag) => tag !== removedTag)
- onChange && onChange(newTags)
- }
+const TagsInput = ({
+ value = [],
+ onChange,
+ propertyName,
+ modelType,
+ placeholder = 'Select or create tags',
+ disabled = false
+}) => {
+ const { getModelPropertyValues } = useContext(ApiServerContext)
+ const [options, setOptions] = useState([])
+ const [searchValue, setSearchValue] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [delayedLoading, setDelayedLoading] = useState(true)
- const handleTagAdd = () => {
- const newTag = inputValue.trim()
- if (newTag && !value.includes(newTag)) {
- const newTags = [...value, newTag]
- onChange && onChange(newTags)
- setInputValue('')
+ const tags = Array.isArray(value) ? value : []
+
+ useEffect(() => {
+ let cancelled = false
+
+ const loadOptions = async () => {
+ if (!modelType || !propertyName) return
+ setLoading(true)
+ try {
+ const values = await getModelPropertyValues(modelType, propertyName)
+ if (cancelled) return
+ const unique = [
+ ...new Set(
+ (values || []).filter(
+ (v) => v != null && v !== '' && typeof v === 'string'
+ )
+ )
+ ]
+ setOptions(unique.map((tag) => ({ label: tag, value: tag })))
+ } finally {
+ if (!cancelled) setLoading(false)
+ }
}
+
+ loadOptions()
+ return () => {
+ cancelled = true
+ }
+ }, [modelType, propertyName, getModelPropertyValues])
+
+ useEffect(() => {
+ if (!loading) {
+ const timer = setTimeout(() => setDelayedLoading(false), 100)
+ return () => clearTimeout(timer)
+ }
+ setDelayedLoading(true)
+ }, [loading])
+
+ const mergedOptions = useMemo(() => {
+ const map = new Map(options.map((option) => [option.value, option]))
+ tags.forEach((tag) => {
+ if (tag != null && tag !== '' && !map.has(tag)) {
+ map.set(tag, { label: tag, value: tag })
+ }
+ })
+ return Array.from(map.values())
+ }, [options, tags])
+
+ const treeData = useMemo(
+ () =>
+ mergedOptions.map((option) => ({
+ title: (
+
+
+
+ ),
+ value: option.value,
+ key: option.value,
+ isLeaf: true
+ })),
+ [mergedOptions]
+ )
+
+ const handleChange = (nextTags) => {
+ onChange?.(nextTags)
+ setSearchValue('')
}
+ const handleAdd = () => {
+ const newTag = searchValue.trim()
+ if (!newTag || tags.includes(newTag)) return
+ onChange?.([...tags, newTag])
+ setSearchValue('')
+ }
+
+ const onInputKeyDown = (e) => {
+ if (e.key !== 'Enter' || e.defaultPrevented) return
+ const newTag = searchValue.trim()
+ if (!newTag || tags.includes(newTag)) return
+ const existsInOptions = mergedOptions.some(
+ (option) => option.value === newTag
+ )
+ if (existsInOptions) return
+ e.preventDefault()
+ e.stopPropagation()
+ onChange?.([...tags, newTag])
+ setSearchValue('')
+ }
+
+ const tagRender = ({ label, value: tagValue, closable, onClose }) => (
+
+ {typeof label === 'string' ? label : tagValue}
+
+ )
+
return (
- <>
-
- {value.map((tag) => (
- handleTagClose(tag)}
- style={{ marginBottom: 12, marginRight: 0 }}
- >
- {tag}
-
- ))}
-
-
- setInputValue(e.target.value)}
- onPressEnter={handleTagAdd}
+
+
+
- } />
-
- >
+ {delayedLoading && (
+
+ )}
+
+ }
+ disabled={disabled || !searchValue.trim()}
+ />
+
)
}
TagsInput.propTypes = {
value: PropTypes.arrayOf(PropTypes.string),
- onChange: PropTypes.func
+ onChange: PropTypes.func,
+ propertyName: PropTypes.string,
+ modelType: PropTypes.string,
+ disabled: PropTypes.bool,
+ placeholder: PropTypes.string
}
export default TagsInput
diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx
index d7f68b5..f460e11 100644
--- a/src/components/Dashboard/context/ApiServerContext.jsx
+++ b/src/components/Dashboard/context/ApiServerContext.jsx
@@ -1403,6 +1403,30 @@ const ApiServerProvider = ({ children }) => {
}
}
+ const getModelPropertyValues = async (objectType, property) => {
+ logger.debug('Fetching property values for model type:', objectType, property)
+ try {
+ const response = await axios.get(
+ `${config.backendUrl}/${getObjectEndpoint(objectType)}/values`,
+ {
+ params: { property },
+ headers: {
+ Accept: 'application/json',
+ Authorization: `Bearer ${token}`
+ }
+ }
+ )
+ logger.debug('Fetched property values for model type:', objectType, property)
+ return Array.isArray(response.data) ? response.data : []
+ } catch (err) {
+ console.error(err)
+ showError(err, () => {
+ getModelPropertyValues(objectType, property)
+ })
+ return []
+ }
+ }
+
const getModelHistory = async (objectType, startDate, endDate) => {
logger.debug('Fetching history for model type:', objectType)
const encodedStartDate = encodeURIComponent(startDate.toISOString())
@@ -1967,6 +1991,7 @@ const ApiServerProvider = ({ children }) => {
searchObjects,
fetchSpotlightData,
getModelStats,
+ getModelPropertyValues,
getModelHistory,
fetchLoading,
showError,