@tldiagram/core-ui 2.0.6 → 2.0.8
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/dist/api/transport-vscode.d.ts +0 -1
- package/dist/components/ZUI/ZUICanvas.d.ts +2 -0
- package/dist/components/ZUI/renderer.d.ts +1 -1
- package/dist/components/ZUI/types.d.ts +1 -0
- package/dist/index.js +8229 -7834
- package/dist/lib/vscodeBridge.d.ts +9 -2
- package/dist/utils/exploreDiffLens.d.ts +47 -0
- package/dist/utils/exploreDiffLens.test.d.ts +1 -0
- package/dist/utils/watchDiffSummary.d.ts +4 -0
- package/dist/utils/watchDiffSummary.test.d.ts +1 -0
- package/package.json +2 -1
- package/src/App.tsx +13 -8
- package/src/api/transport-vscode.ts +3 -11
- package/src/components/ElementLibrary.tsx +1 -0
- package/src/components/ElementNode.tsx +15 -3
- package/src/components/WorkspacePanel.tsx +34 -8
- package/src/components/ZUI/ZUICanvas.tsx +103 -2
- package/src/components/ZUI/renderer.ts +40 -4
- package/src/components/ZUI/types.ts +1 -0
- package/src/components/ZUI/useZUIInteraction.ts +3 -0
- package/src/config/runtime-vscode.ts +2 -4
- package/src/context/WorkspaceVersionContext.tsx +4 -4
- package/src/lib/vscodeBridge.ts +22 -3
- package/src/pages/InfiniteZoom.tsx +210 -0
- package/src/types/offline-ambient.d.ts +1 -0
- package/src/utils/exploreDiffLens.test.ts +185 -0
- package/src/utils/exploreDiffLens.ts +262 -0
- package/src/utils/technologyCatalog.ts +5 -0
- package/src/utils/url.ts +7 -0
- package/src/utils/watchDiffSummary.test.ts +95 -0
- package/src/utils/watchDiffSummary.ts +35 -2
- package/src/vscode-entry.tsx +56 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { WatchDiff } from '../api/client'
|
|
2
|
+
import type { Connector, ExploreData, PlacedElement, ViewTreeNode } from '../types'
|
|
3
|
+
import { isWatchDiffChange, normalizeWatchChangeType, type WatchChangeType } from './watchDiffSummary'
|
|
4
|
+
|
|
5
|
+
export interface ExploreDiffLineDelta {
|
|
6
|
+
added: number
|
|
7
|
+
removed: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ExploreDiffDetail {
|
|
11
|
+
key: string
|
|
12
|
+
resourceType: string
|
|
13
|
+
resourceId?: number
|
|
14
|
+
changeType: WatchChangeType
|
|
15
|
+
summary?: string
|
|
16
|
+
ownerType: string
|
|
17
|
+
ownerKey: string
|
|
18
|
+
language?: string
|
|
19
|
+
addedLines: number
|
|
20
|
+
removedLines: number
|
|
21
|
+
sourcePath?: string
|
|
22
|
+
line?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ExploreDiffTarget extends ExploreDiffDetail {
|
|
26
|
+
label: string
|
|
27
|
+
viewId?: number
|
|
28
|
+
viewName?: string
|
|
29
|
+
unplaced: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ExploreDiffLens {
|
|
33
|
+
versionId: number
|
|
34
|
+
elementChanges: Map<number, WatchChangeType>
|
|
35
|
+
connectorChanges: Map<number, WatchChangeType>
|
|
36
|
+
elementLineDeltas: Map<number, ExploreDiffLineDelta>
|
|
37
|
+
diffDetailsByResource: Map<string, ExploreDiffDetail>
|
|
38
|
+
orderedTargets: ExploreDiffTarget[]
|
|
39
|
+
unplacedTargets: ExploreDiffTarget[]
|
|
40
|
+
changedElementIds: Set<number>
|
|
41
|
+
changedConnectorIds: Set<number>
|
|
42
|
+
ancestorElementIds: Set<number>
|
|
43
|
+
siblingElementIds: Set<number>
|
|
44
|
+
contextElementIds: Set<number>
|
|
45
|
+
contextConnectorIds: Set<number>
|
|
46
|
+
totalAddedLines: number
|
|
47
|
+
totalRemovedLines: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function diffResourceKey(resourceType: string | null | undefined, resourceId: number | null | undefined): string {
|
|
51
|
+
return `${resourceType || 'resource'}:${resourceId ?? 'unknown'}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function flattenViews(nodes: ViewTreeNode[], out = new Map<number, ViewTreeNode>()): Map<number, ViewTreeNode> {
|
|
55
|
+
nodes.forEach((node) => {
|
|
56
|
+
out.set(node.id, node)
|
|
57
|
+
flattenViews(node.children ?? [], out)
|
|
58
|
+
})
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function firstPathLikePart(parts: string[], startIndex: number): string | undefined {
|
|
63
|
+
for (let i = startIndex; i < parts.length; i += 1) {
|
|
64
|
+
const part = parts[i]?.trim()
|
|
65
|
+
if (!part) continue
|
|
66
|
+
if (part.includes('/') || part.includes('\\') || part.includes('.')) return part.replace(/\\/g, '/')
|
|
67
|
+
}
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function sourcePathFromDiff(diff: Pick<WatchDiff, 'owner_type' | 'owner_key'>): string | undefined {
|
|
72
|
+
const ownerType = diff.owner_type?.trim()
|
|
73
|
+
const ownerKey = diff.owner_key?.trim()
|
|
74
|
+
if (!ownerKey) return undefined
|
|
75
|
+
if (ownerType === 'file') return ownerKey.replace(/^file:/, '').replace(/\\/g, '/')
|
|
76
|
+
if (ownerKey.startsWith('file:')) return ownerKey.slice('file:'.length).replace(/\\/g, '/')
|
|
77
|
+
|
|
78
|
+
const parts = ownerKey.split(':')
|
|
79
|
+
if (ownerType === 'symbol' && parts.length >= 4) {
|
|
80
|
+
return parts[1]?.replace(/\\/g, '/')
|
|
81
|
+
}
|
|
82
|
+
if (ownerKey.startsWith('symbol:') && parts.length >= 5) {
|
|
83
|
+
return parts[2]?.replace(/\\/g, '/')
|
|
84
|
+
}
|
|
85
|
+
return firstPathLikePart(parts, 1)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function connectorName(connector: Connector): string {
|
|
89
|
+
return connector.label || connector.relationship || `connector ${connector.id}`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function targetBase(diff: WatchDiff): ExploreDiffDetail {
|
|
93
|
+
const addedLines = Math.max(0, diff.added_lines ?? 0)
|
|
94
|
+
const removedLines = Math.max(0, diff.removed_lines ?? 0)
|
|
95
|
+
const detail: ExploreDiffDetail = {
|
|
96
|
+
key: diffResourceKey(diff.resource_type, diff.resource_id),
|
|
97
|
+
resourceType: diff.resource_type || 'resource',
|
|
98
|
+
resourceId: diff.resource_id,
|
|
99
|
+
changeType: normalizeWatchChangeType(diff.change_type),
|
|
100
|
+
summary: diff.summary,
|
|
101
|
+
ownerType: diff.owner_type,
|
|
102
|
+
ownerKey: diff.owner_key,
|
|
103
|
+
language: diff.language,
|
|
104
|
+
addedLines,
|
|
105
|
+
removedLines,
|
|
106
|
+
sourcePath: sourcePathFromDiff(diff),
|
|
107
|
+
}
|
|
108
|
+
return detail
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildExploreDiffLens(data: ExploreData, diffs: WatchDiff[] | null | undefined, versionId: number): ExploreDiffLens {
|
|
112
|
+
const views = flattenViews(data.tree ?? [])
|
|
113
|
+
const viewByOwnerElement = new Map<number, ViewTreeNode>()
|
|
114
|
+
views.forEach((view) => {
|
|
115
|
+
if (view.owner_element_id != null) viewByOwnerElement.set(view.owner_element_id, view)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const placementsByElement = new Map<number, PlacedElement[]>()
|
|
119
|
+
const connectorsById = new Map<number, Connector>()
|
|
120
|
+
Object.values(data.views ?? {}).forEach((content) => {
|
|
121
|
+
;(content.placements ?? []).forEach((placement) => {
|
|
122
|
+
const list = placementsByElement.get(placement.element_id) ?? []
|
|
123
|
+
list.push(placement)
|
|
124
|
+
placementsByElement.set(placement.element_id, list)
|
|
125
|
+
})
|
|
126
|
+
;(content.connectors ?? []).forEach((connector) => {
|
|
127
|
+
connectorsById.set(connector.id, connector)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const elementChanges = new Map<number, WatchChangeType>()
|
|
132
|
+
const connectorChanges = new Map<number, WatchChangeType>()
|
|
133
|
+
const elementLineDeltas = new Map<number, ExploreDiffLineDelta>()
|
|
134
|
+
const diffDetailsByResource = new Map<string, ExploreDiffDetail>()
|
|
135
|
+
const orderedTargets: ExploreDiffTarget[] = []
|
|
136
|
+
const unplacedTargets: ExploreDiffTarget[] = []
|
|
137
|
+
const changedElementIds = new Set<number>()
|
|
138
|
+
const changedConnectorIds = new Set<number>()
|
|
139
|
+
const ancestorElementIds = new Set<number>()
|
|
140
|
+
const siblingElementIds = new Set<number>()
|
|
141
|
+
const contextElementIds = new Set<number>()
|
|
142
|
+
const contextConnectorIds = new Set<number>()
|
|
143
|
+
let totalAddedLines = 0
|
|
144
|
+
let totalRemovedLines = 0
|
|
145
|
+
|
|
146
|
+
const addContextForViewPath = (viewId: number) => {
|
|
147
|
+
let current = views.get(viewId)
|
|
148
|
+
while (current) {
|
|
149
|
+
const content = data.views[String(current.id)]
|
|
150
|
+
;(content?.placements ?? []).forEach((placement) => {
|
|
151
|
+
siblingElementIds.add(placement.element_id)
|
|
152
|
+
contextElementIds.add(placement.element_id)
|
|
153
|
+
})
|
|
154
|
+
if (current.owner_element_id != null) {
|
|
155
|
+
ancestorElementIds.add(current.owner_element_id)
|
|
156
|
+
contextElementIds.add(current.owner_element_id)
|
|
157
|
+
}
|
|
158
|
+
current = current.parent_view_id != null ? views.get(current.parent_view_id) : undefined
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
;(Array.isArray(diffs) ? diffs : []).forEach((diff) => {
|
|
163
|
+
if (!isWatchDiffChange(diff.change_type)) return
|
|
164
|
+
const detail = targetBase(diff)
|
|
165
|
+
totalAddedLines += detail.addedLines
|
|
166
|
+
totalRemovedLines += detail.removedLines
|
|
167
|
+
diffDetailsByResource.set(detail.key, detail)
|
|
168
|
+
|
|
169
|
+
if (diff.resource_type === 'element' && diff.resource_id) {
|
|
170
|
+
elementChanges.set(diff.resource_id, detail.changeType)
|
|
171
|
+
changedElementIds.add(diff.resource_id)
|
|
172
|
+
if (detail.addedLines > 0 || detail.removedLines > 0) {
|
|
173
|
+
elementLineDeltas.set(diff.resource_id, { added: detail.addedLines, removed: detail.removedLines })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const placements = placementsByElement.get(diff.resource_id) ?? []
|
|
177
|
+
if (placements.length === 0) {
|
|
178
|
+
const ownedView = viewByOwnerElement.get(diff.resource_id)
|
|
179
|
+
unplacedTargets.push({
|
|
180
|
+
...detail,
|
|
181
|
+
label: detail.summary || `element ${diff.resource_id}`,
|
|
182
|
+
viewId: ownedView?.id,
|
|
183
|
+
viewName: ownedView?.name,
|
|
184
|
+
unplaced: true,
|
|
185
|
+
})
|
|
186
|
+
} else {
|
|
187
|
+
placements.forEach((placement) => {
|
|
188
|
+
const view = views.get(placement.view_id)
|
|
189
|
+
addContextForViewPath(placement.view_id)
|
|
190
|
+
orderedTargets.push({
|
|
191
|
+
...detail,
|
|
192
|
+
key: `${detail.key}:view:${placement.view_id}`,
|
|
193
|
+
label: placement.name || detail.summary || `element ${diff.resource_id}`,
|
|
194
|
+
viewId: placement.view_id,
|
|
195
|
+
viewName: view?.name ?? `View ${placement.view_id}`,
|
|
196
|
+
unplaced: false,
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
} else if (diff.resource_type === 'connector' && diff.resource_id) {
|
|
201
|
+
connectorChanges.set(diff.resource_id, detail.changeType)
|
|
202
|
+
changedConnectorIds.add(diff.resource_id)
|
|
203
|
+
const connector = connectorsById.get(diff.resource_id)
|
|
204
|
+
if (connector) {
|
|
205
|
+
addContextForViewPath(connector.view_id)
|
|
206
|
+
contextElementIds.add(connector.source_element_id)
|
|
207
|
+
contextElementIds.add(connector.target_element_id)
|
|
208
|
+
orderedTargets.push({
|
|
209
|
+
...detail,
|
|
210
|
+
label: connectorName(connector),
|
|
211
|
+
viewId: connector.view_id,
|
|
212
|
+
viewName: views.get(connector.view_id)?.name ?? `View ${connector.view_id}`,
|
|
213
|
+
unplaced: false,
|
|
214
|
+
})
|
|
215
|
+
} else {
|
|
216
|
+
unplacedTargets.push({
|
|
217
|
+
...detail,
|
|
218
|
+
label: detail.summary || `connector ${diff.resource_id}`,
|
|
219
|
+
unplaced: true,
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
} else if (detail.sourcePath || diff.resource_type === 'file' || diff.owner_type === 'file') {
|
|
223
|
+
unplacedTargets.push({
|
|
224
|
+
...detail,
|
|
225
|
+
label: detail.summary || detail.sourcePath || detail.ownerKey,
|
|
226
|
+
unplaced: true,
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
Object.values(data.views ?? {}).forEach((content) => {
|
|
232
|
+
;(content.connectors ?? []).forEach((connector) => {
|
|
233
|
+
if (changedConnectorIds.has(connector.id)) return
|
|
234
|
+
if (contextElementIds.has(connector.source_element_id) || contextElementIds.has(connector.target_element_id)) {
|
|
235
|
+
contextConnectorIds.add(connector.id)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
changedElementIds.forEach((id) => {
|
|
241
|
+
contextElementIds.delete(id)
|
|
242
|
+
siblingElementIds.delete(id)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
versionId,
|
|
247
|
+
elementChanges,
|
|
248
|
+
connectorChanges,
|
|
249
|
+
elementLineDeltas,
|
|
250
|
+
diffDetailsByResource,
|
|
251
|
+
orderedTargets,
|
|
252
|
+
unplacedTargets,
|
|
253
|
+
changedElementIds,
|
|
254
|
+
changedConnectorIds,
|
|
255
|
+
ancestorElementIds,
|
|
256
|
+
siblingElementIds,
|
|
257
|
+
contextElementIds,
|
|
258
|
+
contextConnectorIds,
|
|
259
|
+
totalAddedLines,
|
|
260
|
+
totalRemovedLines,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -19,6 +19,11 @@ export function resolveWithBase(urlOrPath: string): string {
|
|
|
19
19
|
if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://') || urlOrPath.startsWith('data:')) {
|
|
20
20
|
return urlOrPath
|
|
21
21
|
}
|
|
22
|
+
const vscodeServerUrl = typeof window !== 'undefined' ? window.__TLD_SERVER_URL__?.replace(/\/+$/, '') : undefined
|
|
23
|
+
if (window.__TLD_VSCODE__ && vscodeServerUrl) {
|
|
24
|
+
const normalizedPath = urlOrPath.startsWith('/') ? urlOrPath : `/${urlOrPath}`
|
|
25
|
+
return `${vscodeServerUrl}${normalizedPath}`
|
|
26
|
+
}
|
|
22
27
|
|
|
23
28
|
// When running inside the native mobile app (Capacitor), or inside an embedded webview
|
|
24
29
|
// that serves content from localhost, avoid prefixing the app BASE_URL. Mobile builds and
|
package/src/utils/url.ts
CHANGED
|
@@ -9,6 +9,13 @@ export function resolveIconPath(path: string | null | undefined): string {
|
|
|
9
9
|
|
|
10
10
|
// Absolute URLs and data URIs are returned as-is
|
|
11
11
|
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('data:')) return path
|
|
12
|
+
const vscodeServerUrl = typeof window !== 'undefined' ? window.__TLD_SERVER_URL__?.replace(/\/+$/, '') : undefined
|
|
13
|
+
const isVsCode = typeof window !== 'undefined' && !!window.__TLD_VSCODE__
|
|
14
|
+
if (isVsCode && vscodeServerUrl) {
|
|
15
|
+
const stripped = path.startsWith('/app/') ? path.slice('/app'.length) : path
|
|
16
|
+
const normalizedPath = stripped.startsWith('/') ? stripped : `/${stripped}`
|
|
17
|
+
return `${vscodeServerUrl}${normalizedPath}`
|
|
18
|
+
}
|
|
12
19
|
|
|
13
20
|
// If running inside the native mobile app (Capacitor) OR inside an embedded
|
|
14
21
|
// webview that serves content from localhost (e.g. Capacitor production webview),
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { WatchDiff } from '../api/client'
|
|
3
|
+
import type { ExploreData } from '../types'
|
|
4
|
+
import {
|
|
5
|
+
buildWatchDiffLocations,
|
|
6
|
+
changedResourceCount,
|
|
7
|
+
formatDiagramResourceSummary,
|
|
8
|
+
formatStatLine,
|
|
9
|
+
isWatchDiffChange,
|
|
10
|
+
summarizeWatchDiffs,
|
|
11
|
+
totalResourceCount,
|
|
12
|
+
} from './watchDiffSummary'
|
|
13
|
+
|
|
14
|
+
function diff(overrides: Partial<WatchDiff>): WatchDiff {
|
|
15
|
+
return {
|
|
16
|
+
id: 1,
|
|
17
|
+
version_id: 1,
|
|
18
|
+
owner_type: 'symbol',
|
|
19
|
+
owner_key: 'go:main.go:function:Main',
|
|
20
|
+
change_type: 'initialized',
|
|
21
|
+
resource_type: 'element',
|
|
22
|
+
resource_id: 101,
|
|
23
|
+
...overrides,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('watch diff summary', () => {
|
|
28
|
+
const data: ExploreData = {
|
|
29
|
+
tree: [{
|
|
30
|
+
id: 1,
|
|
31
|
+
owner_element_id: null,
|
|
32
|
+
name: 'Root',
|
|
33
|
+
description: null,
|
|
34
|
+
level_label: null,
|
|
35
|
+
level: 1,
|
|
36
|
+
depth: 0,
|
|
37
|
+
created_at: '2024-01-01',
|
|
38
|
+
updated_at: '2024-01-01',
|
|
39
|
+
parent_view_id: null,
|
|
40
|
+
children: [],
|
|
41
|
+
}],
|
|
42
|
+
navigations: [],
|
|
43
|
+
views: {
|
|
44
|
+
1: {
|
|
45
|
+
placements: [{
|
|
46
|
+
id: 1,
|
|
47
|
+
view_id: 1,
|
|
48
|
+
element_id: 101,
|
|
49
|
+
position_x: 0,
|
|
50
|
+
position_y: 0,
|
|
51
|
+
name: 'Main',
|
|
52
|
+
description: null,
|
|
53
|
+
kind: 'service',
|
|
54
|
+
technology: null,
|
|
55
|
+
url: null,
|
|
56
|
+
logo_url: null,
|
|
57
|
+
technology_connectors: [],
|
|
58
|
+
tags: [],
|
|
59
|
+
has_view: false,
|
|
60
|
+
view_label: null,
|
|
61
|
+
}],
|
|
62
|
+
connectors: [],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
it('does not describe clean initial resources as changed', () => {
|
|
68
|
+
const summary = summarizeWatchDiffs([
|
|
69
|
+
diff({ id: 1, change_type: 'initialized', resource_type: 'element', resource_id: 101 }),
|
|
70
|
+
diff({ id: 2, change_type: 'initialized', resource_type: 'element', resource_id: 102 }),
|
|
71
|
+
diff({ id: 3, change_type: 'initialized', resource_type: 'connector', resource_id: 201 }),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
expect(changedResourceCount(summary.elements)).toBe(0)
|
|
75
|
+
expect(totalResourceCount(summary.elements)).toBe(2)
|
|
76
|
+
expect(formatDiagramResourceSummary(summary)).toBe('3 initialized resources')
|
|
77
|
+
expect(formatStatLine('element', summary.elements)).toBe('2 elements initialized')
|
|
78
|
+
expect(isWatchDiffChange('initialized')).toBe(false)
|
|
79
|
+
expect(buildWatchDiffLocations(data, [
|
|
80
|
+
diff({ id: 1, change_type: 'initialized', resource_type: 'element', resource_id: 101 }),
|
|
81
|
+
])).toEqual([])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('keeps changed and initialized resources separate in labels', () => {
|
|
85
|
+
const summary = summarizeWatchDiffs([
|
|
86
|
+
diff({ id: 1, change_type: 'updated', resource_type: 'element', resource_id: 101 }),
|
|
87
|
+
diff({ id: 2, change_type: 'deleted', resource_type: 'connector', resource_id: 201 }),
|
|
88
|
+
diff({ id: 3, change_type: 'initialized', resource_type: 'element', resource_id: 102 }),
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
expect(formatDiagramResourceSummary(summary)).toBe('2 changed, 1 initialized resources')
|
|
92
|
+
expect(formatStatLine('connector', summary.connectors)).toBe('1 connector changed')
|
|
93
|
+
expect(formatStatLine('element', summary.elements)).toBe('1 element changed, 1 element initialized')
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -31,11 +31,24 @@ export interface WatchDiffSummary {
|
|
|
31
31
|
connectors: WatchResourceStat
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export function changedResourceCount(stat: WatchResourceStat): number {
|
|
35
|
+
return stat.added + stat.updated + stat.deleted
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function totalResourceCount(stat: WatchResourceStat): number {
|
|
39
|
+
return changedResourceCount(stat) + stat.initialized
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
export function normalizeWatchChangeType(value: string): WatchChangeType {
|
|
35
43
|
if (value === 'added' || value === 'updated' || value === 'deleted' || value === 'initialized') return value
|
|
36
44
|
return 'updated'
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
export function isWatchDiffChange(value: string | null | undefined): boolean {
|
|
48
|
+
const change = normalizeWatchChangeType(value ?? '')
|
|
49
|
+
return change === 'added' || change === 'updated' || change === 'deleted'
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
export function emptyWatchResourceStat(): WatchResourceStat {
|
|
40
53
|
return { added: 0, updated: 0, deleted: 0, initialized: 0, addedLines: 0, removedLines: 0 }
|
|
41
54
|
}
|
|
@@ -64,13 +77,32 @@ export function summarizeWatchDiffs(diffs: WatchDiff[] | null | undefined): Watc
|
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
export function formatStatLine(label: string, stat: WatchResourceStat): string {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
80
|
+
const changed = changedResourceCount(stat)
|
|
81
|
+
const initialized = stat.initialized
|
|
82
|
+
const parts = []
|
|
83
|
+
if (changed > 0) parts.push(`${changed} ${label}${changed === 1 ? '' : 's'} changed`)
|
|
84
|
+
if (initialized > 0) parts.push(`${initialized} ${label}${initialized === 1 ? '' : 's'} initialized`)
|
|
85
|
+
if (parts.length === 0) parts.push(`0 ${label}s changed`)
|
|
69
86
|
if (stat.addedLines > 0) parts.push(`+${stat.addedLines}`)
|
|
70
87
|
if (stat.removedLines > 0) parts.push(`-${stat.removedLines}`)
|
|
71
88
|
return parts.join(', ')
|
|
72
89
|
}
|
|
73
90
|
|
|
91
|
+
export function formatDiagramResourceSummary(summary: WatchDiffSummary): string {
|
|
92
|
+
const changed = changedResourceCount(summary.elements) + changedResourceCount(summary.connectors)
|
|
93
|
+
const initialized = summary.elements.initialized + summary.connectors.initialized
|
|
94
|
+
if (changed > 0 && initialized > 0) {
|
|
95
|
+
return `${changed} changed, ${initialized} initialized resources`
|
|
96
|
+
}
|
|
97
|
+
if (changed > 0) {
|
|
98
|
+
return `${changed} changed resource${changed === 1 ? '' : 's'}`
|
|
99
|
+
}
|
|
100
|
+
if (initialized > 0) {
|
|
101
|
+
return `${initialized} initialized resource${initialized === 1 ? '' : 's'}`
|
|
102
|
+
}
|
|
103
|
+
return 'No diagram resource changes'
|
|
104
|
+
}
|
|
105
|
+
|
|
74
106
|
export function formatTldStatLine(summary: WatchDiffSummary): string {
|
|
75
107
|
return [
|
|
76
108
|
formatStatLine('element', summary.elements),
|
|
@@ -131,6 +163,7 @@ export function buildWatchDiffLocations(data: ExploreData, diffs: WatchDiff[] |
|
|
|
131
163
|
|
|
132
164
|
const locations: WatchDiffLocation[] = []
|
|
133
165
|
;(Array.isArray(diffs) ? diffs : []).forEach((diff) => {
|
|
166
|
+
if (!isWatchDiffChange(diff.change_type)) return
|
|
134
167
|
if (!diff.resource_id) return
|
|
135
168
|
const base = {
|
|
136
169
|
changeType: normalizeWatchChangeType(diff.change_type),
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import { ChakraProvider } from '@chakra-ui/react'
|
|
4
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
6
|
+
import App from './App'
|
|
7
|
+
import theme from './theme'
|
|
8
|
+
import { ToastContainer } from './utils/toast'
|
|
9
|
+
import { PlatformProvider } from './platform/PlatformContext'
|
|
10
|
+
import { platform as localPlatform } from './platform/local'
|
|
11
|
+
import './index.css'
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {
|
|
15
|
+
__TLD_DIAGRAM_ID__?: number
|
|
16
|
+
__TLD_VSCODE__?: boolean
|
|
17
|
+
__TLD_VSCODE_API__?: {
|
|
18
|
+
postMessage: (msg: unknown) => void
|
|
19
|
+
}
|
|
20
|
+
__TLD_SERVER_URL__?: string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const diagramId = window.__TLD_DIAGRAM_ID__
|
|
25
|
+
const initialPath = diagramId != null ? `/views/${diagramId}` : '/views'
|
|
26
|
+
|
|
27
|
+
const queryClient = new QueryClient({
|
|
28
|
+
defaultOptions: {
|
|
29
|
+
queries: {
|
|
30
|
+
staleTime: 5_000,
|
|
31
|
+
refetchOnWindowFocus: false,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
createRoot(document.getElementById('root')!).render(
|
|
37
|
+
<StrictMode>
|
|
38
|
+
<QueryClientProvider client={queryClient}>
|
|
39
|
+
<ChakraProvider theme={theme}>
|
|
40
|
+
<PlatformProvider platform={localPlatform}>
|
|
41
|
+
<MemoryRouter
|
|
42
|
+
initialEntries={[initialPath]}
|
|
43
|
+
future={{
|
|
44
|
+
v7_startTransition: true,
|
|
45
|
+
v7_relativeSplatPath: true,
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<App />
|
|
49
|
+
</MemoryRouter>
|
|
50
|
+
<ToastContainer />
|
|
51
|
+
</PlatformProvider>
|
|
52
|
+
</ChakraProvider>
|
|
53
|
+
</QueryClientProvider>
|
|
54
|
+
</StrictMode>,
|
|
55
|
+
)
|
|
56
|
+
|