diff --git a/packages/web/projects/vgpu/views/card/admin/index.vue b/packages/web/projects/vgpu/views/card/admin/index.vue index a8bc6a4..d878136 100644 --- a/packages/web/projects/vgpu/views/card/admin/index.vue +++ b/packages/web/projects/vgpu/views/card/admin/index.vue @@ -24,7 +24,7 @@ clearable :placeholder="$t('card.allCardTypes')" :options="cardTypeOptions" - @change="applyFilters" + @change="onTypeChange" /> [ - { label: t('card.allNodes'), value: undefined }, - ...rawNodeNames.value.map((name) => ({ label: name, value: name })), -]); +const nodeOptions = computed(() => { + // When a card type is selected, only offer nodes that actually carry that type. + const nodes = filters.type + ? rawNodes.value.filter((node) => node.types.includes(filters.type)) + : rawNodes.value; + return [ + { label: t('card.allNodes'), value: undefined }, + ...nodes.map((node) => ({ label: node.name, value: node.name })), + ]; +}); const cardTypeOptions = computed(() => [ { label: t('card.allCardTypes'), value: undefined }, ...rawCardTypes.value.map((type) => ({ label: type, value: type })), @@ -121,18 +127,37 @@ const fetchFilterOptions = async () => { request(nodeApi.getNodeList({ filters: {} })), request(cardApi.getCardType()), ]); - rawNodeNames.value = nodeList - .map((item) => item?.name) - .filter(Boolean); + rawNodes.value = nodeList + .filter((item) => item?.name) + .map((item) => ({ name: item.name, types: Array.isArray(item.type) ? item.type : [] })); rawCardTypes.value = typeList .map((item) => item?.type) .filter(Boolean); } catch { - rawNodeNames.value = []; + rawNodes.value = []; rawCardTypes.value = []; } }; +// When the selected type no longer includes the chosen node, drop the node so +// the table isn't filtered by an out-of-scope node. Call synchronously before +// applyFilters at every type-mutation site to avoid querying with a stale node. +const pruneNodeScopeByType = () => { + if (!filters.type || !filters.nodeName) { + return; + } + const node = rawNodes.value.find((item) => item.name === filters.nodeName); + if (!node || !node.types.includes(filters.type)) { + filters.nodeName = undefined; + hasManualNodeScope.value = true; + } +}; + +const onTypeChange = () => { + pruneNodeScopeByType(); + applyFilters(); +}; + const handleClick = (params) => { router.push({ path: `/admin/vgpu/card/admin/${params.data.name}`, @@ -336,6 +361,7 @@ const handlePieClick = (params, echarts) => { dataIndex: params.dataIndex, }); filters.type = name; + pruneNodeScopeByType(); applyFilters(); }; @@ -400,6 +426,7 @@ watch( const next = parseTypeFromQuery(value); if (filters.type === next) return; filters.type = next; + pruneNodeScopeByType(); applyFilters(); }, ); diff --git a/packages/web/projects/vgpu/views/task/admin/index.vue b/packages/web/projects/vgpu/views/task/admin/index.vue index 153505e..38007f3 100644 --- a/packages/web/projects/vgpu/views/task/admin/index.vue +++ b/packages/web/projects/vgpu/views/task/admin/index.vue @@ -27,6 +27,13 @@ :options="statusOptions" @change="applyFilters" /> + import taskApi from '~/vgpu/api/task'; import nodeApi from '~/vgpu/api/node'; +import cardApi from '~/vgpu/api/card'; import Toolbar from '@/components/TablePlus/Toolbar.vue'; import TablePagination from '@/components/TablePlus/Pagination.vue'; import { roundToDecimal, timeParse } from '@/utils'; @@ -95,12 +103,24 @@ const filters = reactive({ name: props.filters?.name || '', nodeName: props.filters?.nodeName, status: props.filters?.status, + deviceId: props.filters?.deviceId, }); const rawNodeNames = ref([]); +const rawCards = ref([]); const nodeOptions = computed(() => [ { label: t('task.allNodes'), value: undefined }, ...rawNodeNames.value.map((name) => ({ label: name, value: name })), ]); +const cardOptions = computed(() => { + // When a node is selected, only offer the cards that live on that node. + const cards = filters.nodeName + ? rawCards.value.filter((card) => card.nodeName === filters.nodeName) + : rawCards.value; + return [ + { label: t('task.allCards'), value: undefined }, + ...cards.map((card) => ({ label: card.uuid, value: card.uuid })), + ]; +}); const statusOptions = computed(() => [ { label: t('task.allStatus'), value: undefined }, { label: t('task.statusCompleted'), value: 'closed' }, @@ -119,12 +139,19 @@ const state = reactive({ const fetchFilterOptions = async () => { try { - const { list: nodeList = [] } = await request(nodeApi.getNodeList({ filters: {} })); + const [{ list: nodeList = [] }, { list: cardList = [] }] = await Promise.all([ + request(nodeApi.getNodeList({ filters: {} })), + request(cardApi.getCardList({ filters: {} })), + ]); rawNodeNames.value = nodeList .map((item) => item?.name) .filter(Boolean); + rawCards.value = cardList + .filter((item) => item?.uuid) + .map((item) => ({ uuid: item.uuid, nodeName: item.nodeName })); } catch { rawNodeNames.value = []; + rawCards.value = []; } }; @@ -251,6 +278,7 @@ const fetchTableData = async () => { ...(nodeName ? { nodeName } : {}), ...(nodeUid ? { nodeUid } : {}), ...(filters.status ? { status: filters.status } : {}), + ...(filters.deviceId ? { deviceId: filters.deviceId } : {}), }, }; const { items = [] } = await taskApi.getTaskListReq(payload); @@ -266,6 +294,17 @@ const { getTrimValue, applyFilters, refreshTable } = useTableFilters({ }); const onNodeNameChange = () => { hasManualNodeScope.value = true; + // The card dropdown is scoped to the selected node; drop a previously chosen + // card if it doesn't belong to that node so the table isn't filtered by an + // out-of-scope device. + if (filters.nodeName && filters.deviceId) { + const stillValid = rawCards.value.some( + (card) => card.nodeName === filters.nodeName && card.uuid === filters.deviceId, + ); + if (!stillValid) { + filters.deviceId = undefined; + } + } applyFilters(); }; @@ -279,12 +318,14 @@ watch( props.filters?.nodeName, props.filters?.nodeUid, props.filters?.status, + props.filters?.deviceId, ], () => { hasManualNodeScope.value = false; filters.name = props.filters?.name || ''; filters.nodeName = props.filters?.nodeName; filters.status = props.filters?.status; + filters.deviceId = props.filters?.deviceId; applyFilters(); }, { immediate: true },