@cat-factory/app 0.135.2 → 0.136.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/panels/InspectorPanel.vue +24 -79
- package/app/modular/nav-contributions.spec.ts +6 -1
- package/app/modular/panels/inspector.logic.spec.ts +99 -0
- package/app/modular/panels/inspector.logic.ts +113 -0
- package/app/modular/panels/inspector.ts +101 -0
- package/app/modular/registry.ts +1 -1
- package/app/modular/slots.ts +9 -2
- package/app/plugins/modular.client.ts +12 -2
- package/package.json +7 -7
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { PanelsOutlet } from '@modular-vue/core'
|
|
2
3
|
import type { Block, BlockStatus } from '~/types/domain'
|
|
3
4
|
import { blockTypeMeta, STATUS_META } from '~/utils/catalog'
|
|
4
5
|
import { pipelineAllowedForManualStart } from '~/utils/pipeline'
|
|
5
|
-
import
|
|
6
|
-
import TaskContextIssues from '~/components/tasks/TaskContextIssues.vue'
|
|
7
|
-
import TaskAgentConfig from '~/components/panels/inspector/TaskAgentConfig.vue'
|
|
8
|
-
import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
|
|
9
|
-
import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
|
|
10
|
-
import ServiceReleaseHealthConfig from '~/components/panels/inspector/ServiceReleaseHealthConfig.vue'
|
|
11
|
-
import ServiceTestSecrets from '~/components/panels/inspector/ServiceTestSecrets.vue'
|
|
12
|
-
import FrontendConfig from '~/components/panels/inspector/FrontendConfig.vue'
|
|
13
|
-
import ServiceConnections from '~/components/panels/inspector/ServiceConnections.vue'
|
|
14
|
-
import ContainerSummary from '~/components/panels/inspector/ContainerSummary.vue'
|
|
15
|
-
import TaskDependencies from '~/components/panels/inspector/TaskDependencies.vue'
|
|
16
|
-
import TaskStructure from '~/components/panels/inspector/TaskStructure.vue'
|
|
17
|
-
import TaskRunSettings from '~/components/panels/inspector/TaskRunSettings.vue'
|
|
18
|
-
import TaskExecution from '~/components/panels/inspector/TaskExecution.vue'
|
|
19
|
-
import TaskEstimateBadge from '~/components/panels/inspector/TaskEstimateBadge.vue'
|
|
20
|
-
import EpicChildren from '~/components/panels/inspector/EpicChildren.vue'
|
|
21
|
-
import InitiativeInspector from '~/components/panels/inspector/InitiativeInspector.vue'
|
|
6
|
+
import { inspectorPanels } from '~/modular/panels/inspector.logic'
|
|
22
7
|
import IconButton from '~/components/common/IconButton.vue'
|
|
23
|
-
import RecurringScheduleSettings from '~/components/panels/inspector/RecurringScheduleSettings.vue'
|
|
24
8
|
import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
|
|
25
9
|
import AgentStopButton from '~/components/board/AgentStopButton.vue'
|
|
26
10
|
|
|
@@ -76,8 +60,6 @@ watch(
|
|
|
76
60
|
)
|
|
77
61
|
const isContainer = computed(() => level.value === 'frame' || level.value === 'module')
|
|
78
62
|
const isTask = computed(() => level.value === 'task')
|
|
79
|
-
const isEpic = computed(() => level.value === 'epic')
|
|
80
|
-
const isInitiative = computed(() => level.value === 'initiative')
|
|
81
63
|
|
|
82
64
|
const instance = computed(() => execution.getInstance(block.value?.executionId))
|
|
83
65
|
const typeMeta = computed(() => (block.value ? blockTypeMeta(block.value.type) : null))
|
|
@@ -481,14 +463,9 @@ const showOriginalDescription = ref(false)
|
|
|
481
463
|
</UButton>
|
|
482
464
|
</div>
|
|
483
465
|
|
|
484
|
-
<!-- task: context documents -->
|
|
485
|
-
<TaskContextDocs v-if="isTask" :key="`context-docs-${block.id}`" :block="block" />
|
|
486
|
-
|
|
487
|
-
<!-- task: context issues (tracker) -->
|
|
488
|
-
<TaskContextIssues v-if="isTask" :key="`context-issues-${block.id}`" :block="block" />
|
|
489
|
-
|
|
490
466
|
<!-- service (frame): navigate the prescriptive spec tree (+ Gherkin scenarios when
|
|
491
|
-
the spec is on the repo's default branch)
|
|
467
|
+
the spec is on the repo's default branch). A shell affordance, not a
|
|
468
|
+
level-keyed body panel, so it stays here above the panel outlet. -->
|
|
492
469
|
<UButton
|
|
493
470
|
v-if="isFrame"
|
|
494
471
|
block
|
|
@@ -501,60 +478,28 @@ const showOriginalDescription = ref(false)
|
|
|
501
478
|
{{ t('panels.inspector.viewRequirements') }}
|
|
502
479
|
</UButton>
|
|
503
480
|
|
|
504
|
-
<!--
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
481
|
+
<!-- The level/type-keyed inspector body: the `inspectorPanels` panel group
|
|
482
|
+
(slice 4 of the modular-vue adoption). `<PanelsOutlet>` renders every
|
|
483
|
+
panel whose `when(block)` matches, ordered, with the selected block
|
|
484
|
+
injected as the subject (read via `usePanelSubject` in each panel's
|
|
485
|
+
wrapper). Replaces the pre-slice-4 `v-if` fan; `subject-key` is the block
|
|
486
|
+
id, so switching selections remounts panel content (matching the old
|
|
487
|
+
per-panel `:key`). A consumer contributes its own panels to the SAME
|
|
488
|
+
group via `registerAppModule`.
|
|
489
|
+
|
|
490
|
+
The `subject` cast is an upstream typing quirk, not a modelling escape
|
|
491
|
+
hatch: `<PanelsOutlet>` declares `subject` as `PropType<unknown>` with
|
|
492
|
+
`default: null`, which Volar narrows to `null`, so passing a typed
|
|
493
|
+
`Block | null` is rejected at compile time. `unknown` is the real
|
|
494
|
+
runtime contract; `as any` is the minimal unblock until the binding
|
|
495
|
+
types the prop explicitly (filed upstream — see the slice-4 residuals
|
|
496
|
+
in docs/initiatives/modular-vue-slice4-upstream-zones.md). -->
|
|
497
|
+
<PanelsOutlet
|
|
498
|
+
:group="inspectorPanels"
|
|
499
|
+
:subject="(block ?? null) as any"
|
|
500
|
+
:subject-key="block?.id ?? ''"
|
|
518
501
|
/>
|
|
519
502
|
|
|
520
|
-
<!-- service (frame): test infra + provisioning configuration -->
|
|
521
|
-
<ServiceTestConfig v-if="isFrame" :key="`test-config-${block.id}`" :block="block" />
|
|
522
|
-
|
|
523
|
-
<!-- service (frame): SENSITIVE test credentials (sealed, injected out of band) -->
|
|
524
|
-
<ServiceTestSecrets v-if="isFrame" :key="`test-secrets-${block.id}`" :block="block" />
|
|
525
|
-
|
|
526
|
-
<!-- service (frame): best-practice fragments for code-aware agents -->
|
|
527
|
-
<ServiceFragments v-if="isFrame" :key="`fragments-${block.id}`" :block="block" />
|
|
528
|
-
|
|
529
|
-
<!-- service (frame): post-release-health monitor/SLO mapping -->
|
|
530
|
-
<ServiceReleaseHealthConfig
|
|
531
|
-
v-if="isFrame"
|
|
532
|
-
:key="`release-health-${block.id}`"
|
|
533
|
-
:block="block"
|
|
534
|
-
/>
|
|
535
|
-
|
|
536
|
-
<!-- task: the live execution surface first (open by default), then the estimate,
|
|
537
|
-
then the collapsed configuration sections (dependencies, run settings, agent
|
|
538
|
-
config, structure) so a running task reads top-down without scrolling. -->
|
|
539
|
-
<!-- Keyed by block id so a manual collapse/expand doesn't leak across task
|
|
540
|
-
selections: switching tasks re-mounts each section back to its default state
|
|
541
|
-
(e.g. the live Execution section is open again for the newly selected task). -->
|
|
542
|
-
<template v-else-if="isTask">
|
|
543
|
-
<RecurringScheduleSettings :key="`schedule-${block.id}`" :block="block" />
|
|
544
|
-
<TaskExecution :key="`execution-${block.id}`" :block="block" />
|
|
545
|
-
<TaskEstimateBadge :key="`estimate-${block.id}`" :block="block" />
|
|
546
|
-
<TaskDependencies :key="`deps-${block.id}`" :block="block" />
|
|
547
|
-
<TaskRunSettings :key="`run-settings-${block.id}`" :block="block" />
|
|
548
|
-
<TaskAgentConfig :key="`agent-config-${block.id}`" :block="block" />
|
|
549
|
-
<TaskStructure :key="`structure-${block.id}`" :block="block" />
|
|
550
|
-
</template>
|
|
551
|
-
|
|
552
|
-
<!-- epic: the full tree of member tasks, grouped by service → module -->
|
|
553
|
-
<EpicChildren v-else-if="isEpic" :key="`epic-${block.id}`" :block="block" />
|
|
554
|
-
|
|
555
|
-
<!-- initiative: status + goal, run-planning + tracker controls -->
|
|
556
|
-
<InitiativeInspector v-else-if="isInitiative" :block="block" />
|
|
557
|
-
|
|
558
503
|
<!-- Locked-run explanation: a disabled task Run button reads as a dead lock unless
|
|
559
504
|
it says what's holding it. Named here (and on the button title) so the blocking
|
|
560
505
|
dependencies are visible to pointer, keyboard, and touch alike — a native title
|
|
@@ -45,7 +45,12 @@ const ALL_GATES: NavGates = {
|
|
|
45
45
|
isAccountAdmin: true,
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const slots = (): AppSlots => ({
|
|
48
|
+
const slots = (): AppSlots => ({
|
|
49
|
+
nav: [...NAV_CONTRIBUTIONS],
|
|
50
|
+
resultViews: [],
|
|
51
|
+
agentKinds: [],
|
|
52
|
+
inspectorPanels: [],
|
|
53
|
+
})
|
|
49
54
|
const ids = (s: unknown) => (s as AppSlots).nav.map((i) => i.id)
|
|
50
55
|
|
|
51
56
|
describe('navSlotFilter', () => {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { resolvePanels } from '@modular-vue/core'
|
|
3
|
+
import type { PanelEntry } from '@modular-vue/core'
|
|
4
|
+
import type { Block, BlockLevel, BlockType } from '~/types/domain'
|
|
5
|
+
import { INSPECTOR_PANEL_IDS, INSPECTOR_PANEL_SPECS } from './inspector.logic'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pins the inspector panel group's gating + ordering (slice 4 of the modular-vue
|
|
9
|
+
* adoption) against the pre-slice-4 `InspectorPanel.vue` `v-if` fan, using the
|
|
10
|
+
* SAME pure `resolvePanels` the host resolves through. A stub component per spec
|
|
11
|
+
* stands in for the real SFC (the gating/order is component-agnostic).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const ENTRIES: PanelEntry<Block>[] = INSPECTOR_PANEL_SPECS.map((spec) => ({
|
|
15
|
+
id: spec.id,
|
|
16
|
+
order: spec.order,
|
|
17
|
+
when: spec.when,
|
|
18
|
+
// A trivial functional component stands in for the real SFC — the gating/order
|
|
19
|
+
// under test is component-agnostic; `() => null` satisfies the engine's
|
|
20
|
+
// `UiComponent` (a callable) without pulling a Vue runtime into the unit test.
|
|
21
|
+
component: () => null,
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
const block = (level: BlockLevel, type: BlockType = 'service'): Block =>
|
|
25
|
+
({ id: `b-${level}-${type}`, level, type }) as unknown as Block
|
|
26
|
+
|
|
27
|
+
const visibleIds = (b: Block | null) => resolvePanels(ENTRIES, b).map((e) => e.id)
|
|
28
|
+
|
|
29
|
+
describe('inspector panel group', () => {
|
|
30
|
+
it('every id has exactly one spec (no dup, no gap)', () => {
|
|
31
|
+
const specIds = INSPECTOR_PANEL_SPECS.map((s) => s.id).sort()
|
|
32
|
+
expect(specIds).toEqual([...INSPECTOR_PANEL_IDS].sort())
|
|
33
|
+
expect(new Set(specIds).size).toBe(specIds.length)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('a service frame shows the container + service panels, ordered, no frontend-config', () => {
|
|
37
|
+
expect(visibleIds(block('frame', 'service'))).toEqual([
|
|
38
|
+
'container-summary',
|
|
39
|
+
'service-connections',
|
|
40
|
+
'service-test-config',
|
|
41
|
+
'service-test-secrets',
|
|
42
|
+
'service-fragments',
|
|
43
|
+
'service-release-health',
|
|
44
|
+
])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('a frontend frame swaps connections for frontend-config', () => {
|
|
48
|
+
expect(visibleIds(block('frame', 'frontend'))).toEqual([
|
|
49
|
+
'container-summary',
|
|
50
|
+
'frontend-config',
|
|
51
|
+
'service-test-config',
|
|
52
|
+
'service-test-secrets',
|
|
53
|
+
'service-fragments',
|
|
54
|
+
'service-release-health',
|
|
55
|
+
])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('a module shows only the container summary', () => {
|
|
59
|
+
expect(visibleIds(block('module'))).toEqual(['container-summary'])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('a task shows the task body in the pre-slice-4 order', () => {
|
|
63
|
+
expect(visibleIds(block('task'))).toEqual([
|
|
64
|
+
'task-context-docs',
|
|
65
|
+
'task-context-issues',
|
|
66
|
+
'recurring-schedule',
|
|
67
|
+
'task-execution',
|
|
68
|
+
'task-estimate',
|
|
69
|
+
'task-dependencies',
|
|
70
|
+
'task-run-settings',
|
|
71
|
+
'task-agent-config',
|
|
72
|
+
'task-structure',
|
|
73
|
+
])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('epic and initiative each show their single inspector', () => {
|
|
77
|
+
expect(visibleIds(block('epic'))).toEqual(['epic-children'])
|
|
78
|
+
expect(visibleIds(block('initiative'))).toEqual(['initiative-inspector'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('no subject selected resolves to no panels', () => {
|
|
82
|
+
expect(visibleIds(null)).toEqual([])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Locks the contract the client plugin's boot fail-fast depends on: it calls
|
|
86
|
+
// `resolvePanels(mergedEntries, null)` once at startup so a duplicate panel id
|
|
87
|
+
// (e.g. a consumer module colliding with a first-party id) throws at BOOT rather
|
|
88
|
+
// than the first time a block is selected. That only works because `resolvePanels`
|
|
89
|
+
// runs its duplicate-id check BEFORE the null-subject short-circuit — pin it here
|
|
90
|
+
// so an upstream reorder (dedup after the null guard) can't silently disarm the
|
|
91
|
+
// boot check.
|
|
92
|
+
it('throws on a duplicate panel id even with a null subject (boot fail-fast)', () => {
|
|
93
|
+
const dup: PanelEntry<Block>[] = [
|
|
94
|
+
{ id: 'container-summary', order: 1, when: () => true, component: () => null },
|
|
95
|
+
{ id: 'container-summary', order: 2, when: () => true, component: () => null },
|
|
96
|
+
]
|
|
97
|
+
expect(() => resolvePanels(dup, null)).toThrow(/duplicate panel id "container-summary"/)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { definePanelGroup } from '@modular-vue/core'
|
|
2
|
+
import type { Block } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure definition of the block-inspector panel group (slice 4 of the modular-vue
|
|
6
|
+
* adoption — docs/initiatives/modular-vue-adoption.md).
|
|
7
|
+
*
|
|
8
|
+
* The inspector used to be a 631-line `v-if` fan in `InspectorPanel.vue` that
|
|
9
|
+
* switched its body on the selected block's `level` (frame/module/task/epic/
|
|
10
|
+
* initiative) and, inside the frame branch, on `type` (frontend/service). Slice 4
|
|
11
|
+
* turns that fan into the upstream **subject-keyed panels** primitive: each body
|
|
12
|
+
* sub-panel is a `PanelEntry<Block>` contributed to the `inspectorPanels` slot,
|
|
13
|
+
* and `<PanelsOutlet>` renders every entry whose `when(block)` passes, ordered by
|
|
14
|
+
* `order`, with the selected block injected as the subject. A consumer deployment
|
|
15
|
+
* contributes its OWN inspector panels (e.g. for a custom block type) to the SAME
|
|
16
|
+
* slot via `registerAppModule`, with zero host edits.
|
|
17
|
+
*
|
|
18
|
+
* This file is deliberately free of `.vue`/`defineModule` imports so the gating +
|
|
19
|
+
* ordering is unit-tested directly (`inspector.logic.spec.ts`) via the pure
|
|
20
|
+
* `resolvePanels`; the component wiring (which imports the sub-panel SFCs) lives
|
|
21
|
+
* in `inspector.ts`, registered from the client plugin — the same split as the
|
|
22
|
+
* result-views and journey modules. (`definePanelGroup` is a pure `{ slotKey }`
|
|
23
|
+
* token, so the group handle belongs here where every call site shares it.)
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The slot key the inspector panel entries are contributed under (and that the
|
|
27
|
+
* group handle names). Also the `AppSlots` key — keep the two in step. */
|
|
28
|
+
export const INSPECTOR_PANELS_SLOT = 'inspectorPanels' as const
|
|
29
|
+
|
|
30
|
+
/** The typed panel-group handle — `usePanels(inspectorPanels, block)` /
|
|
31
|
+
* `<PanelsOutlet :group="inspectorPanels" :subject="block">` resolve against it. */
|
|
32
|
+
export const inspectorPanels = definePanelGroup<Block>(INSPECTOR_PANELS_SLOT)
|
|
33
|
+
|
|
34
|
+
/** Every built-in inspector panel id. A const union so the component map in
|
|
35
|
+
* `inspector.ts` is exhaustive (a spec with no component is a compile error, not
|
|
36
|
+
* a silently-dropped panel). */
|
|
37
|
+
export const INSPECTOR_PANEL_IDS = [
|
|
38
|
+
// task body (rendered in this order under the identity block)
|
|
39
|
+
'task-context-docs',
|
|
40
|
+
'task-context-issues',
|
|
41
|
+
'recurring-schedule',
|
|
42
|
+
'task-execution',
|
|
43
|
+
'task-estimate',
|
|
44
|
+
'task-dependencies',
|
|
45
|
+
'task-run-settings',
|
|
46
|
+
'task-agent-config',
|
|
47
|
+
'task-structure',
|
|
48
|
+
// service / module body
|
|
49
|
+
'container-summary',
|
|
50
|
+
'frontend-config',
|
|
51
|
+
'service-connections',
|
|
52
|
+
'service-test-config',
|
|
53
|
+
'service-test-secrets',
|
|
54
|
+
'service-fragments',
|
|
55
|
+
'service-release-health',
|
|
56
|
+
// epic / initiative body
|
|
57
|
+
'epic-children',
|
|
58
|
+
'initiative-inspector',
|
|
59
|
+
] as const
|
|
60
|
+
|
|
61
|
+
export type InspectorPanelId = (typeof INSPECTOR_PANEL_IDS)[number]
|
|
62
|
+
|
|
63
|
+
/** The gating + ordering for one built-in inspector panel — the `PanelEntry`
|
|
64
|
+
* minus its component (attached in `inspector.ts`). `when` is the predicate
|
|
65
|
+
* lifted verbatim from the panel's old `v-if`; `order` preserves the pre-slice-4
|
|
66
|
+
* render order within a level (levels never overlap, so cross-level order is
|
|
67
|
+
* irrelevant and the values are just grouped by level). */
|
|
68
|
+
export interface InspectorPanelSpec {
|
|
69
|
+
id: InspectorPanelId
|
|
70
|
+
order: number
|
|
71
|
+
when: (block: Block) => boolean
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isTask = (b: Block) => b.level === 'task'
|
|
75
|
+
const isFrame = (b: Block) => b.level === 'frame'
|
|
76
|
+
/** frame OR module — the "container" panels. */
|
|
77
|
+
const isContainer = (b: Block) => b.level === 'frame' || b.level === 'module'
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The built-in panel specs. The `when` predicates and orders reproduce
|
|
81
|
+
* `InspectorPanel.vue`'s pre-slice-4 behaviour exactly (pinned by
|
|
82
|
+
* `inspector.logic.spec.ts`):
|
|
83
|
+
* - task body: context docs/issues, then schedule → execution → estimate →
|
|
84
|
+
* dependencies → run-settings → agent-config → structure.
|
|
85
|
+
* - service/module body: container summary (frame|module), then the frame-only
|
|
86
|
+
* panels, with frontend-config / service-connections further gated on `type`.
|
|
87
|
+
* - epic / initiative bodies: their single inspector.
|
|
88
|
+
*
|
|
89
|
+
* The frame-only "view requirements" button and the cross-cutting run banners
|
|
90
|
+
* (`AgentFailureCard` / `AgentStopButton`) stay in the host shell — they are not
|
|
91
|
+
* level-keyed body panels, so converting them would add ceremony without
|
|
92
|
+
* extensibility value.
|
|
93
|
+
*/
|
|
94
|
+
export const INSPECTOR_PANEL_SPECS: readonly InspectorPanelSpec[] = [
|
|
95
|
+
{ id: 'task-context-docs', order: 10, when: isTask },
|
|
96
|
+
{ id: 'task-context-issues', order: 20, when: isTask },
|
|
97
|
+
{ id: 'recurring-schedule', order: 30, when: isTask },
|
|
98
|
+
{ id: 'task-execution', order: 40, when: isTask },
|
|
99
|
+
{ id: 'task-estimate', order: 50, when: isTask },
|
|
100
|
+
{ id: 'task-dependencies', order: 60, when: isTask },
|
|
101
|
+
{ id: 'task-run-settings', order: 70, when: isTask },
|
|
102
|
+
{ id: 'task-agent-config', order: 80, when: isTask },
|
|
103
|
+
{ id: 'task-structure', order: 90, when: isTask },
|
|
104
|
+
{ id: 'container-summary', order: 110, when: isContainer },
|
|
105
|
+
{ id: 'frontend-config', order: 120, when: (b) => isFrame(b) && b.type === 'frontend' },
|
|
106
|
+
{ id: 'service-connections', order: 130, when: (b) => isFrame(b) && b.type === 'service' },
|
|
107
|
+
{ id: 'service-test-config', order: 140, when: isFrame },
|
|
108
|
+
{ id: 'service-test-secrets', order: 150, when: isFrame },
|
|
109
|
+
{ id: 'service-fragments', order: 160, when: isFrame },
|
|
110
|
+
{ id: 'service-release-health', order: 170, when: isFrame },
|
|
111
|
+
{ id: 'epic-children', order: 200, when: (b) => b.level === 'epic' },
|
|
112
|
+
{ id: 'initiative-inspector', order: 210, when: (b) => b.level === 'initiative' },
|
|
113
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type Component, defineComponent, h } from 'vue'
|
|
2
|
+
import { defineModule, usePanelSubject } from '@modular-vue/core'
|
|
3
|
+
import type { PanelEntry } from '@modular-vue/core'
|
|
4
|
+
import type { Block } from '~/types/domain'
|
|
5
|
+
|
|
6
|
+
/** The engine's opaque component type on a `PanelEntry` (the neutral `UiComponent`,
|
|
7
|
+
* which isn't exported by name). A Vue component is a valid one; the outlet renders
|
|
8
|
+
* it as a Vue component. We reference it structurally so no cast reaches for `any`. */
|
|
9
|
+
type PanelComponent = PanelEntry<Block>['component']
|
|
10
|
+
import {
|
|
11
|
+
INSPECTOR_PANELS_SLOT,
|
|
12
|
+
INSPECTOR_PANEL_SPECS,
|
|
13
|
+
type InspectorPanelId,
|
|
14
|
+
} from '~/modular/panels/inspector.logic'
|
|
15
|
+
import TaskContextDocs from '~/components/documents/TaskContextDocs.vue'
|
|
16
|
+
import TaskContextIssues from '~/components/tasks/TaskContextIssues.vue'
|
|
17
|
+
import RecurringScheduleSettings from '~/components/panels/inspector/RecurringScheduleSettings.vue'
|
|
18
|
+
import TaskExecution from '~/components/panels/inspector/TaskExecution.vue'
|
|
19
|
+
import TaskEstimateBadge from '~/components/panels/inspector/TaskEstimateBadge.vue'
|
|
20
|
+
import TaskDependencies from '~/components/panels/inspector/TaskDependencies.vue'
|
|
21
|
+
import TaskRunSettings from '~/components/panels/inspector/TaskRunSettings.vue'
|
|
22
|
+
import TaskAgentConfig from '~/components/panels/inspector/TaskAgentConfig.vue'
|
|
23
|
+
import TaskStructure from '~/components/panels/inspector/TaskStructure.vue'
|
|
24
|
+
import ContainerSummary from '~/components/panels/inspector/ContainerSummary.vue'
|
|
25
|
+
import FrontendConfig from '~/components/panels/inspector/FrontendConfig.vue'
|
|
26
|
+
import ServiceConnections from '~/components/panels/inspector/ServiceConnections.vue'
|
|
27
|
+
import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
|
|
28
|
+
import ServiceTestSecrets from '~/components/panels/inspector/ServiceTestSecrets.vue'
|
|
29
|
+
import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
|
|
30
|
+
import ServiceReleaseHealthConfig from '~/components/panels/inspector/ServiceReleaseHealthConfig.vue'
|
|
31
|
+
import EpicChildren from '~/components/panels/inspector/EpicChildren.vue'
|
|
32
|
+
import InitiativeInspector from '~/components/panels/inspector/InitiativeInspector.vue'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Component wiring for the block-inspector panel group (slice 4). The gating +
|
|
36
|
+
* ordering live in `inspector.logic.ts` (pure, unit-tested); this file attaches
|
|
37
|
+
* the sub-panel SFCs and registers the module. It imports `.vue`, so — like
|
|
38
|
+
* `result-views.ts` and the journey step module — it is registered from the
|
|
39
|
+
* client plugin via `extraModules`, keeping the unit-tested `registry.ts` import
|
|
40
|
+
* graph SFC-free.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Bridge a `block`-prop sub-panel onto the subject-keyed panels primitive.
|
|
45
|
+
* `<PanelsOutlet>` injects the selected block as the panel *subject* (a `subject`
|
|
46
|
+
* prop + a `provide`), but the existing inspector sub-panels take a `block` prop
|
|
47
|
+
* (and several — `TaskExecution`, `ServiceTestConfig` — are reused OUTSIDE the
|
|
48
|
+
* inspector, so their prop can't be renamed). So each entry's component is this
|
|
49
|
+
* thin wrapper: it reads the injected subject via `usePanelSubject` and forwards
|
|
50
|
+
* it as `:block`. The host sets `<PanelsOutlet :subject-key="b => b.id">`, so the
|
|
51
|
+
* wrapper remounts on block switch — matching the pre-slice-4 per-panel
|
|
52
|
+
* `:key="…-${block.id}"`.
|
|
53
|
+
*/
|
|
54
|
+
function blockPanel(component: Component, id: InspectorPanelId): PanelComponent {
|
|
55
|
+
return defineComponent({
|
|
56
|
+
name: `InspectorPanel__${id}`,
|
|
57
|
+
setup() {
|
|
58
|
+
const block = usePanelSubject<Block>()
|
|
59
|
+
return () => h(component, { block: block.value })
|
|
60
|
+
},
|
|
61
|
+
}) as unknown as PanelComponent
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Exhaustive id → sub-panel map. Typed `Record<InspectorPanelId, …>` so adding a
|
|
65
|
+
* spec without a component (or vice-versa) fails the typecheck. */
|
|
66
|
+
const COMPONENTS: Record<InspectorPanelId, Component> = {
|
|
67
|
+
'task-context-docs': TaskContextDocs,
|
|
68
|
+
'task-context-issues': TaskContextIssues,
|
|
69
|
+
'recurring-schedule': RecurringScheduleSettings,
|
|
70
|
+
'task-execution': TaskExecution,
|
|
71
|
+
'task-estimate': TaskEstimateBadge,
|
|
72
|
+
'task-dependencies': TaskDependencies,
|
|
73
|
+
'task-run-settings': TaskRunSettings,
|
|
74
|
+
'task-agent-config': TaskAgentConfig,
|
|
75
|
+
'task-structure': TaskStructure,
|
|
76
|
+
'container-summary': ContainerSummary,
|
|
77
|
+
'frontend-config': FrontendConfig,
|
|
78
|
+
'service-connections': ServiceConnections,
|
|
79
|
+
'service-test-config': ServiceTestConfig,
|
|
80
|
+
'service-test-secrets': ServiceTestSecrets,
|
|
81
|
+
'service-fragments': ServiceFragments,
|
|
82
|
+
'service-release-health': ServiceReleaseHealthConfig,
|
|
83
|
+
'epic-children': EpicChildren,
|
|
84
|
+
'initiative-inspector': InitiativeInspector,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The built-in `PanelEntry`s: each spec's gating/order + its wrapped component. */
|
|
88
|
+
export const INSPECTOR_PANEL_ENTRIES: PanelEntry<Block>[] = INSPECTOR_PANEL_SPECS.map((spec) => ({
|
|
89
|
+
id: spec.id,
|
|
90
|
+
order: spec.order,
|
|
91
|
+
when: spec.when,
|
|
92
|
+
component: blockPanel(COMPONENTS[spec.id], spec.id),
|
|
93
|
+
}))
|
|
94
|
+
|
|
95
|
+
/** The first-party module: contributes the built-in panels to the `inspectorPanels`
|
|
96
|
+
* slot. Registered by the client plugin (`extraModules`). */
|
|
97
|
+
export const inspectorPanelsModule = defineModule({
|
|
98
|
+
id: 'cat-factory:inspector-panels',
|
|
99
|
+
version: '1.0.0',
|
|
100
|
+
slots: { [INSPECTOR_PANELS_SLOT]: INSPECTOR_PANEL_ENTRIES },
|
|
101
|
+
})
|
package/app/modular/registry.ts
CHANGED
|
@@ -101,7 +101,7 @@ export function createAppRegistry(
|
|
|
101
101
|
// are registered from the client plugin via `extraModules` + `registerJourney`.
|
|
102
102
|
const registry = createRegistry<AppDeps, AppSlots>({
|
|
103
103
|
services: { gates: deps.gates },
|
|
104
|
-
slots: { nav: [], resultViews: [], agentKinds: [] },
|
|
104
|
+
slots: { nav: [], resultViews: [], agentKinds: [], inspectorPanels: [] },
|
|
105
105
|
}).use(journeysPlugin())
|
|
106
106
|
for (const mod of [...FIRST_PARTY_MODULES, ...extraModules, ...consumerModules]) {
|
|
107
107
|
registry.register(mod)
|
package/app/modular/slots.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Component } from 'vue'
|
|
2
|
-
import type { ComponentEntry } from '@modular-vue/core'
|
|
3
|
-
import type { CustomAgentKind } from '~/types/domain'
|
|
2
|
+
import type { ComponentEntry, PanelEntry } from '@modular-vue/core'
|
|
3
|
+
import type { Block, CustomAgentKind } from '~/types/domain'
|
|
4
4
|
import type { NavContribution } from './nav-contributions'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -17,6 +17,12 @@ import type { NavContribution } from './nav-contributions'
|
|
|
17
17
|
* contributes (the palette/catalog data half). BACKEND-registered kinds
|
|
18
18
|
* arrive separately as a {@link RemoteModuleManifest} read in the agents
|
|
19
19
|
* store — see `stores/agents.ts`.
|
|
20
|
+
* - `inspectorPanels` (slice 4) — the subject-keyed detail panels of the block
|
|
21
|
+
* inspector ({@link PanelEntry} over a `Block`), rendered all-matching + ordered
|
|
22
|
+
* by `<PanelsOutlet>` and gated per-block by each entry's `when(block)`
|
|
23
|
+
* predicate. Replaces `InspectorPanel.vue`'s level/type `v-if` fan; a consumer
|
|
24
|
+
* contributes its own panels to the SAME slot via `registerAppModule`. This is
|
|
25
|
+
* the slot key `definePanelGroup<Block>('inspectorPanels')` names.
|
|
20
26
|
*
|
|
21
27
|
* The index signature is mutable (`unknown[]`) to satisfy the runtime's
|
|
22
28
|
* `SlotMap` constraint while `unknown[]` still meets `useReactiveSlots`'
|
|
@@ -26,6 +32,7 @@ export interface AppSlots {
|
|
|
26
32
|
nav: NavContribution[]
|
|
27
33
|
resultViews: ResultViewContribution[]
|
|
28
34
|
agentKinds: CustomAgentKind[]
|
|
35
|
+
inspectorPanels: PanelEntry<Block>[]
|
|
29
36
|
[key: string]: unknown[]
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { installModularApp } from '@modular-vue/nuxt/runtime'
|
|
2
2
|
import type { ApplicationManifest } from '@modular-vue/runtime'
|
|
3
3
|
import type { NavigationItem } from '@modular-frontend/core'
|
|
4
|
-
import { resolveComponentRegistry } from '@modular-vue/core'
|
|
4
|
+
import { resolveComponentRegistry, resolvePanels } from '@modular-vue/core'
|
|
5
|
+
import type { PanelEntry } from '@modular-vue/core'
|
|
5
6
|
import { provideJourneyRuntime } from '@modular-vue/journeys'
|
|
6
7
|
import type { JourneyRuntime } from '@modular-vue/journeys'
|
|
7
8
|
import { createAppRegistry } from '~/modular/registry'
|
|
8
9
|
import { navSlotFilter } from '~/modular/nav-contributions'
|
|
9
10
|
import { createNavGates } from '~/modular/nav-gates'
|
|
10
11
|
import { resultViewsModule } from '~/modular/result-views'
|
|
12
|
+
import { inspectorPanelsModule } from '~/modular/panels/inspector'
|
|
11
13
|
import {
|
|
12
14
|
environmentSetupJourney,
|
|
13
15
|
environmentSetupModule,
|
|
14
16
|
environmentSetupPersistence,
|
|
15
17
|
} from '~/modular/journeys/environmentSetup'
|
|
16
18
|
import type { AppSlots, ResultViewContribution } from '~/modular/slots'
|
|
17
|
-
import type { CustomAgentKind } from '~/types/domain'
|
|
19
|
+
import type { Block, CustomAgentKind } from '~/types/domain'
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Wire the modular-vue registry into the Nuxt app (slice 0 of the modular-vue
|
|
@@ -58,6 +60,7 @@ export default defineNuxtPlugin({
|
|
|
58
60
|
// resolves (inside `installModularApp`).
|
|
59
61
|
const registry = createAppRegistry({ gates: createNavGates() }, [
|
|
60
62
|
resultViewsModule,
|
|
63
|
+
inspectorPanelsModule,
|
|
61
64
|
environmentSetupModule,
|
|
62
65
|
])
|
|
63
66
|
registry.registerJourney(environmentSetupJourney, {
|
|
@@ -88,6 +91,13 @@ export default defineNuxtPlugin({
|
|
|
88
91
|
// clear stack. The slot is static after this resolve, so `StepResultViewHost`'s own
|
|
89
92
|
// reactive re-resolve is a cheap memoized read that this has already validated.
|
|
90
93
|
resolveComponentRegistry((slots.resultViews ?? []) as ResultViewContribution[])
|
|
94
|
+
// Same fail-fast for the inspector panel group (slice 4): resolve the merged
|
|
95
|
+
// `inspectorPanels` slot once with a null subject so a duplicate panel id across
|
|
96
|
+
// the first-party + consumer modules throws at BOOT (`resolvePanels` dedupes/
|
|
97
|
+
// throws before the null-subject guard) rather than the first time a block is
|
|
98
|
+
// selected. `<PanelsOutlet>` re-resolves reactively per subject; this only
|
|
99
|
+
// validates the wiring.
|
|
100
|
+
resolvePanels((slots.inspectorPanels ?? []) as PanelEntry<Block>[], null)
|
|
91
101
|
// Consumer agent kinds contributed as CODE to the static `agentKinds` slot
|
|
92
102
|
// (module slots resolve once, so the static base is the full set).
|
|
93
103
|
useAgentsStore().registerConsumerKinds((slots.agentKinds ?? []) as CustomAgentKind[])
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.136.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@modular-frontend/core": "^0.
|
|
22
|
-
"@modular-vue/core": "^1.
|
|
23
|
-
"@modular-vue/journeys": "^1.
|
|
24
|
-
"@modular-vue/nuxt": "^0.
|
|
25
|
-
"@modular-vue/runtime": "^1.
|
|
26
|
-
"@modular-vue/vue": "^1.
|
|
21
|
+
"@modular-frontend/core": "^0.4.0",
|
|
22
|
+
"@modular-vue/core": "^1.3.0",
|
|
23
|
+
"@modular-vue/journeys": "^1.3.0",
|
|
24
|
+
"@modular-vue/nuxt": "^0.4.0",
|
|
25
|
+
"@modular-vue/runtime": "^1.4.1",
|
|
26
|
+
"@modular-vue/vue": "^1.3.0",
|
|
27
27
|
"@nuxt/ui": "^4.10.0",
|
|
28
28
|
"@nuxtjs/i18n": "^10.4.1",
|
|
29
29
|
"@pinia/nuxt": "^1.0.1",
|