@teambit/workspace 1.0.1079 → 1.0.1081
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/{preview-1785260578779.js → preview-1785278532531.js} +2 -2
- package/dist/ui/workspace/use-workspace.js +179 -27
- package/dist/ui/workspace/use-workspace.js.map +1 -1
- package/dist/ui/workspace/workspace-overview/index.d.ts +1 -1
- package/dist/ui/workspace/workspace-overview/index.js +5 -5
- package/dist/ui/workspace/workspace-overview/index.js.map +1 -1
- package/dist/ui/workspace/workspace-overview/workspace-overview.js +13 -143
- package/dist/ui/workspace/workspace-overview/workspace-overview.js.map +1 -1
- package/dist/ui/workspace/workspace-skeleton.d.ts +8 -0
- package/dist/ui/workspace/workspace-skeleton.js +73 -0
- package/dist/ui/workspace/workspace-skeleton.js.map +1 -0
- package/dist/ui/workspace/workspace-skeleton.module.scss +102 -0
- package/dist/ui/workspace/workspace.js +23 -7
- package/dist/ui/workspace/workspace.js.map +1 -1
- package/package.json +65 -74
- package/ui/workspace/use-workspace.ts +185 -27
- package/ui/workspace/workspace-overview/index.ts +1 -1
- package/ui/workspace/workspace-overview/workspace-overview.tsx +9 -102
- package/ui/workspace/workspace-skeleton.module.scss +102 -0
- package/ui/workspace/workspace-skeleton.tsx +35 -0
- package/ui/workspace/workspace.tsx +16 -5
- package/dist/ui/workspace/workspace-overview/card-overlays.d.ts +0 -5
- package/dist/ui/workspace/workspace-overview/card-overlays.js +0 -133
- package/dist/ui/workspace/workspace-overview/card-overlays.js.map +0 -1
- package/dist/ui/workspace/workspace-overview/card-overlays.module.scss +0 -72
- package/dist/ui/workspace/workspace-overview/filter-utils.d.ts +0 -10
- package/dist/ui/workspace/workspace-overview/filter-utils.js +0 -40
- package/dist/ui/workspace/workspace-overview/filter-utils.js.map +0 -1
- package/dist/ui/workspace/workspace-overview/hope-component-card.d.ts +0 -15
- package/dist/ui/workspace/workspace-overview/hope-component-card.js +0 -186
- package/dist/ui/workspace/workspace-overview/hope-component-card.js.map +0 -1
- package/dist/ui/workspace/workspace-overview/hope-component-card.module.scss +0 -169
- package/dist/ui/workspace/workspace-overview/link-plugin.d.ts +0 -3
- package/dist/ui/workspace/workspace-overview/link-plugin.js +0 -14
- package/dist/ui/workspace/workspace-overview/link-plugin.js.map +0 -1
- package/dist/ui/workspace/workspace-overview/namespace-header.d.ts +0 -9
- package/dist/ui/workspace/workspace-overview/namespace-header.js +0 -107
- package/dist/ui/workspace/workspace-overview/namespace-header.js.map +0 -1
- package/dist/ui/workspace/workspace-overview/namespace-header.module.scss +0 -88
- package/dist/ui/workspace/workspace-overview/namespace-sort.d.ts +0 -8
- package/dist/ui/workspace/workspace-overview/namespace-sort.js +0 -47
- package/dist/ui/workspace/workspace-overview/namespace-sort.js.map +0 -1
- package/dist/ui/workspace/workspace-overview/scope-sort.d.ts +0 -1
- package/dist/ui/workspace/workspace-overview/scope-sort.js +0 -14
- package/dist/ui/workspace/workspace-overview/scope-sort.js.map +0 -1
- package/dist/ui/workspace/workspace-overview/use-query-param-with-default.d.ts +0 -6
- package/dist/ui/workspace/workspace-overview/use-query-param-with-default.js +0 -102
- package/dist/ui/workspace/workspace-overview/use-query-param-with-default.js.map +0 -1
- package/dist/ui/workspace/workspace-overview/use-workspace-aggregation.d.ts +0 -3
- package/dist/ui/workspace/workspace-overview/use-workspace-aggregation.js +0 -88
- package/dist/ui/workspace/workspace-overview/use-workspace-aggregation.js.map +0 -1
- package/dist/ui/workspace/workspace-overview/workspace-filter-panel.d.ts +0 -13
- package/dist/ui/workspace/workspace-overview/workspace-filter-panel.js +0 -110
- package/dist/ui/workspace/workspace-overview/workspace-filter-panel.js.map +0 -1
- package/dist/ui/workspace/workspace-overview/workspace-overview.module.scss +0 -91
- package/dist/ui/workspace/workspace-overview/workspace-overview.sort.d.ts +0 -1
- package/dist/ui/workspace/workspace-overview/workspace-overview.sort.js +0 -30
- package/dist/ui/workspace/workspace-overview/workspace-overview.sort.js.map +0 -1
- package/dist/ui/workspace/workspace-overview/workspace-overview.types.d.ts +0 -28
- package/dist/ui/workspace/workspace-overview/workspace-overview.types.js +0 -3
- package/dist/ui/workspace/workspace-overview/workspace-overview.types.js.map +0 -1
- package/ui/workspace/workspace-overview/card-overlays.module.scss +0 -72
- package/ui/workspace/workspace-overview/card-overlays.tsx +0 -66
- package/ui/workspace/workspace-overview/filter-utils.ts +0 -42
- package/ui/workspace/workspace-overview/hope-component-card.module.scss +0 -169
- package/ui/workspace/workspace-overview/hope-component-card.tsx +0 -152
- package/ui/workspace/workspace-overview/link-plugin.ts +0 -5
- package/ui/workspace/workspace-overview/namespace-header.module.scss +0 -88
- package/ui/workspace/workspace-overview/namespace-header.tsx +0 -72
- package/ui/workspace/workspace-overview/namespace-sort.ts +0 -44
- package/ui/workspace/workspace-overview/scope-sort.ts +0 -9
- package/ui/workspace/workspace-overview/use-query-param-with-default.ts +0 -115
- package/ui/workspace/workspace-overview/use-workspace-aggregation.ts +0 -79
- package/ui/workspace/workspace-overview/workspace-filter-panel.tsx +0 -109
- package/ui/workspace/workspace-overview/workspace-overview.module.scss +0 -91
- package/ui/workspace/workspace-overview/workspace-overview.sort.ts +0 -29
- package/ui/workspace/workspace-overview/workspace-overview.types.ts +0 -30
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useEffect, useMemo } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { ComponentModel } from '@teambit/component';
|
|
3
3
|
import useLatest from '@react-hook/latest';
|
|
4
4
|
import { useDataQuery } from '@teambit/ui-foundation.ui.hooks.use-data-query';
|
|
5
|
-
import { gql } from '@apollo/client';
|
|
5
|
+
import { gql, useApolloClient } from '@apollo/client';
|
|
6
6
|
import type { ComponentIdObj } from '@teambit/component-id';
|
|
7
7
|
import { ComponentID } from '@teambit/component-id';
|
|
8
8
|
|
|
@@ -19,8 +19,15 @@ type UseWorkspaceOptions = {
|
|
|
19
19
|
};
|
|
20
20
|
type RawComponent = { id: ComponentIdObj };
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Cheap, card-renderable fields only. Everything here resolves from already-loaded component
|
|
24
|
+
* metadata (id, env, aspects, compositions, preview, deprecation, server, buildStatus, description),
|
|
25
|
+
* so the light query returns fast and the grid can paint immediately. The two expensive async
|
|
26
|
+
* resolvers — `status` (getStatus(): git/scope compare) and `issuesCount` (getIssues(): dependency
|
|
27
|
+
* analysis) — are deliberately omitted here and fetched separately by `wcComponentDetailFields`.
|
|
28
|
+
*/
|
|
29
|
+
const wcComponentFieldsLight = gql`
|
|
30
|
+
fragment wcComponentFieldsLight on Component {
|
|
24
31
|
id {
|
|
25
32
|
name
|
|
26
33
|
version
|
|
@@ -37,18 +44,6 @@ const wcComponentFields = gql`
|
|
|
37
44
|
identifier
|
|
38
45
|
}
|
|
39
46
|
description
|
|
40
|
-
issuesCount
|
|
41
|
-
status {
|
|
42
|
-
isOutdated
|
|
43
|
-
isNew
|
|
44
|
-
isInScope
|
|
45
|
-
isStaged
|
|
46
|
-
modifyInfo {
|
|
47
|
-
hasModifiedFiles
|
|
48
|
-
hasModifiedDependencies
|
|
49
|
-
}
|
|
50
|
-
isDeleted
|
|
51
|
-
}
|
|
52
47
|
buildStatus
|
|
53
48
|
preview {
|
|
54
49
|
includesEnvTemplate
|
|
@@ -72,40 +67,100 @@ const wcComponentFields = gql`
|
|
|
72
67
|
}
|
|
73
68
|
`;
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The expensive per-component fields, split out so they never block first paint. These drive the
|
|
72
|
+
* status pills (changed/building) and the issues badge, both of which gracefully degrade to a
|
|
73
|
+
* neutral "built" / no-issues state until this query resolves.
|
|
74
|
+
*/
|
|
75
|
+
const wcComponentDetailFields = gql`
|
|
76
|
+
fragment wcComponentDetailFields on Component {
|
|
77
|
+
id {
|
|
78
|
+
name
|
|
79
|
+
version
|
|
80
|
+
scope
|
|
81
|
+
}
|
|
82
|
+
issuesCount
|
|
83
|
+
status {
|
|
84
|
+
isOutdated
|
|
85
|
+
isNew
|
|
86
|
+
isInScope
|
|
87
|
+
isStaged
|
|
88
|
+
modifyInfo {
|
|
89
|
+
hasModifiedFiles
|
|
90
|
+
hasModifiedDependencies
|
|
91
|
+
}
|
|
92
|
+
isDeleted
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Full per-component shape (light + detail). Used by the subscriptions: a single added/changed
|
|
99
|
+
* component is cheap to fully resolve, so live updates always carry fresh status.
|
|
100
|
+
*/
|
|
101
|
+
const wcComponentFieldsFull = gql`
|
|
102
|
+
fragment wcComponentFieldsFull on Component {
|
|
103
|
+
...wcComponentFieldsLight
|
|
104
|
+
...wcComponentDetailFields
|
|
105
|
+
}
|
|
106
|
+
${wcComponentFieldsLight}
|
|
107
|
+
${wcComponentDetailFields}
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const WORKSPACE_LIGHT = gql`
|
|
111
|
+
query workspaceLight {
|
|
77
112
|
workspace {
|
|
78
113
|
name
|
|
79
114
|
path
|
|
80
115
|
icon
|
|
81
116
|
components {
|
|
82
|
-
...
|
|
117
|
+
...wcComponentFieldsLight
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
${wcComponentFieldsLight}
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
// The detail fields are fetched one page at a time via the existing `components(offset, limit)`
|
|
125
|
+
// resolver. getStatus()/getIssues() are synchronous CPU work; fetching all 204 at once blocks the
|
|
126
|
+
// server's event loop for ~5s straight (a navigation query issued during that window waited ~10s).
|
|
127
|
+
// Paging it lets the loop breathe between chunks — measured ping latency dropped from ~10s to ~0.3s.
|
|
128
|
+
const WORKSPACE_DETAILS_PAGE = gql`
|
|
129
|
+
query workspaceDetailsPage($offset: Int!, $limit: Int!) {
|
|
130
|
+
workspace {
|
|
131
|
+
components(offset: $offset, limit: $limit) {
|
|
132
|
+
...wcComponentDetailFields
|
|
83
133
|
}
|
|
84
134
|
}
|
|
85
135
|
}
|
|
86
|
-
${
|
|
136
|
+
${wcComponentDetailFields}
|
|
87
137
|
`;
|
|
88
138
|
|
|
139
|
+
// Small pages keep any single server-blocking burst under ~350ms; the gap yields the event loop so
|
|
140
|
+
// other requests (navigation, lanes) are serviced promptly between pages.
|
|
141
|
+
const DETAIL_CHUNK_SIZE = 15;
|
|
142
|
+
const DETAIL_CHUNK_GAP_MS = 60;
|
|
143
|
+
|
|
89
144
|
const COMPONENT_SUBSCRIPTION_ADDED = gql`
|
|
90
145
|
subscription OnComponentAdded {
|
|
91
146
|
componentAdded {
|
|
92
147
|
component {
|
|
93
|
-
...
|
|
148
|
+
...wcComponentFieldsFull
|
|
94
149
|
}
|
|
95
150
|
}
|
|
96
151
|
}
|
|
97
|
-
${
|
|
152
|
+
${wcComponentFieldsFull}
|
|
98
153
|
`;
|
|
99
154
|
|
|
100
155
|
const COMPONENT_SUBSCRIPTION_CHANGED = gql`
|
|
101
156
|
subscription OnComponentChanged {
|
|
102
157
|
componentChanged {
|
|
103
158
|
component {
|
|
104
|
-
...
|
|
159
|
+
...wcComponentFieldsFull
|
|
105
160
|
}
|
|
106
161
|
}
|
|
107
162
|
}
|
|
108
|
-
${
|
|
163
|
+
${wcComponentFieldsFull}
|
|
109
164
|
`;
|
|
110
165
|
|
|
111
166
|
const COMPONENT_SUBSCRIPTION_REMOVED = gql`
|
|
@@ -131,10 +186,94 @@ const COMPONENT_SERVER_STARTED = gql`
|
|
|
131
186
|
}
|
|
132
187
|
`;
|
|
133
188
|
|
|
189
|
+
function componentKey(id: ComponentIdObj): string {
|
|
190
|
+
return `${id.scope}/${id.name}@${id.version}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
134
193
|
export function useWorkspace(options: UseWorkspaceOptions = {}): UseWorkspaceResult {
|
|
194
|
+
// Light query drives first paint. cache-and-network so a revisit paints instantly from cache,
|
|
195
|
+
// then refreshes in the background (the workspace host otherwise defaults to network-only).
|
|
135
196
|
// `previousData` is destructured out (not spread into `...rest`) so the returned shape matches
|
|
136
197
|
// UseWorkspaceResult, which omits it (along with `data`, replaced by `workspace`).
|
|
137
|
-
const {
|
|
198
|
+
const {
|
|
199
|
+
data,
|
|
200
|
+
previousData: _previousData,
|
|
201
|
+
subscribeToMore,
|
|
202
|
+
loading,
|
|
203
|
+
...rest
|
|
204
|
+
} = useDataQuery(WORKSPACE_LIGHT, {
|
|
205
|
+
fetchPolicy: 'cache-and-network',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// CRITICAL: the detail data (status + issuesCount) is NOT fetched in parallel with the light query.
|
|
209
|
+
// getStatus()/getIssues() are synchronous CPU work that monopolizes the server's single-threaded
|
|
210
|
+
// event loop for several seconds on large workspaces; running it concurrently would stall the light
|
|
211
|
+
// query behind it (measured: light alone ~10ms, but ~5s when racing the detail query) and the grid
|
|
212
|
+
// would never paint early. So we wait for the light data to arrive AND for the browser to go idle
|
|
213
|
+
// (i.e. the grid has painted) before kicking off the (chunked) detail fetch in the background.
|
|
214
|
+
const [detailEnabled, setDetailEnabled] = useState(false);
|
|
215
|
+
const lightDataReady = Boolean(data?.workspace);
|
|
216
|
+
const totalComponents = data?.workspace?.components?.length ?? 0;
|
|
217
|
+
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!lightDataReady || detailEnabled || typeof window === 'undefined') return undefined;
|
|
220
|
+
const enable = () => setDetailEnabled(true);
|
|
221
|
+
const ric = (window as any).requestIdleCallback as
|
|
222
|
+
| ((cb: () => void, opts?: { timeout: number }) => number)
|
|
223
|
+
| undefined;
|
|
224
|
+
if (ric) {
|
|
225
|
+
const handle = ric(enable, { timeout: 1000 });
|
|
226
|
+
return () => (window as any).cancelIdleCallback?.(handle);
|
|
227
|
+
}
|
|
228
|
+
const handle = window.setTimeout(enable, 200);
|
|
229
|
+
return () => window.clearTimeout(handle);
|
|
230
|
+
}, [lightDataReady, detailEnabled]);
|
|
231
|
+
|
|
232
|
+
// Detail (status + issuesCount) is paged in once the grid has painted, with a yield between pages
|
|
233
|
+
// so the server stays responsive. Results accumulate by component key and merge in progressively —
|
|
234
|
+
// status pills and issue counts fill in chunk by chunk rather than all-or-nothing after ~5s.
|
|
235
|
+
const client = useApolloClient();
|
|
236
|
+
const [detailByKey, setDetailByKey] = useState<Map<string, { status?: any; issuesCount?: number }>>(new Map());
|
|
237
|
+
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (!detailEnabled || !totalComponents) return undefined;
|
|
240
|
+
let cancelled = false;
|
|
241
|
+
|
|
242
|
+
(async () => {
|
|
243
|
+
for (let offset = 0; offset < totalComponents; offset += DETAIL_CHUNK_SIZE) {
|
|
244
|
+
if (cancelled) return;
|
|
245
|
+
try {
|
|
246
|
+
const res = await client.query({
|
|
247
|
+
query: WORKSPACE_DETAILS_PAGE,
|
|
248
|
+
variables: { offset, limit: DETAIL_CHUNK_SIZE },
|
|
249
|
+
fetchPolicy: 'network-only',
|
|
250
|
+
});
|
|
251
|
+
if (cancelled) return;
|
|
252
|
+
const comps: { id: ComponentIdObj; status?: any; issuesCount?: number }[] =
|
|
253
|
+
res.data?.workspace?.components ?? [];
|
|
254
|
+
if (comps.length) {
|
|
255
|
+
setDetailByKey((prev) => {
|
|
256
|
+
const next = new Map(prev);
|
|
257
|
+
for (const c of comps) next.set(componentKey(c.id), { status: c.status, issuesCount: c.issuesCount });
|
|
258
|
+
return next;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// a failed page must not abort the rest of the pages
|
|
263
|
+
}
|
|
264
|
+
if (cancelled) return;
|
|
265
|
+
// yield the server event loop between pages
|
|
266
|
+
await new Promise((resolve) => {
|
|
267
|
+
setTimeout(resolve, DETAIL_CHUNK_GAP_MS);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
271
|
+
|
|
272
|
+
return () => {
|
|
273
|
+
cancelled = true;
|
|
274
|
+
};
|
|
275
|
+
}, [detailEnabled, totalComponents, client]);
|
|
276
|
+
|
|
138
277
|
const optionsRef = useLatest(options);
|
|
139
278
|
|
|
140
279
|
useEffect(() => {
|
|
@@ -249,11 +388,30 @@ export function useWorkspace(options: UseWorkspaceOptions = {}): UseWorkspaceRes
|
|
|
249
388
|
}, [optionsRef]);
|
|
250
389
|
|
|
251
390
|
const workspace = useMemo(() => {
|
|
252
|
-
|
|
253
|
-
|
|
391
|
+
const rawWorkspace = data?.workspace;
|
|
392
|
+
if (!rawWorkspace) return undefined;
|
|
393
|
+
|
|
394
|
+
// Merge the paged-in heavy fields (status, issuesCount) onto the light components by id.
|
|
395
|
+
// Components are not normalized in the Apollo cache (their `id` is an object, not a scalar key),
|
|
396
|
+
// so the detail pages are accumulated in `detailByKey` and merged here by hand. The light
|
|
397
|
+
// component wins when it already carries a value — a live subscription update (full fragment)
|
|
398
|
+
// refreshes status there, and the paged detail is only the initial enrichment.
|
|
399
|
+
const components = rawWorkspace.components.map((component: any) => {
|
|
400
|
+
const detail = detailByKey.get(componentKey(component.id));
|
|
401
|
+
if (!detail) return component;
|
|
402
|
+
return {
|
|
403
|
+
...component,
|
|
404
|
+
status: component.status ?? detail.status,
|
|
405
|
+
issuesCount: component.issuesCount ?? detail.issuesCount,
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return Workspace.from({ ...rawWorkspace, components });
|
|
410
|
+
}, [data?.workspace, detailByKey]);
|
|
254
411
|
|
|
255
412
|
return {
|
|
256
413
|
workspace,
|
|
414
|
+
loading,
|
|
257
415
|
subscribeToMore,
|
|
258
416
|
...rest,
|
|
259
417
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { WorkspaceOverview } from './workspace-overview';
|
|
2
|
-
export { useQueryParamWithDefault, useListParamWithDefault } from '
|
|
2
|
+
export { useQueryParamWithDefault, useListParamWithDefault } from '@teambit/explorer.ui.components-overview';
|
|
@@ -1,116 +1,23 @@
|
|
|
1
|
-
import React, { useContext
|
|
2
|
-
import { ComponentGrid } from '@teambit/explorer.ui.gallery.component-grid';
|
|
3
|
-
// Legacy empty state — kept around as a fallback while the new blank state rolls out.
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5
|
-
import { EmptyWorkspace } from '@teambit/workspace.ui.empty-workspace';
|
|
6
|
-
import compact from 'lodash.compact';
|
|
7
|
-
import { ScopeID } from '@teambit/scopes.scope-id';
|
|
8
|
-
import { useCloudScopes } from '@teambit/cloud.hooks.use-cloud-scopes';
|
|
1
|
+
import React, { useContext } from 'react';
|
|
9
2
|
import { useWorkspaceMode } from '@teambit/workspace.ui.use-workspace-mode';
|
|
3
|
+
import { ComponentsOverview } from '@teambit/explorer.ui.components-overview';
|
|
10
4
|
import { WorkspaceContext } from '../workspace-context';
|
|
11
|
-
import { useWorkspaceAggregation } from './use-workspace-aggregation';
|
|
12
|
-
import { useQueryParamWithDefault, useListParamWithDefault } from './use-query-param-with-default';
|
|
13
|
-
import { NamespaceHeader } from './namespace-header';
|
|
14
|
-
import { HopeComponentCard } from './hope-component-card';
|
|
15
|
-
import type { AggregationType } from './workspace-overview.types';
|
|
16
|
-
import { WorkspaceFilterPanel } from './workspace-filter-panel';
|
|
17
5
|
import { WorkspaceBlankState } from './workspace-blank-state';
|
|
18
|
-
import styles from './workspace-overview.module.scss';
|
|
19
6
|
|
|
20
7
|
export function WorkspaceOverview() {
|
|
21
8
|
const workspace = useContext(WorkspaceContext);
|
|
22
9
|
const { components, componentDescriptors } = workspace;
|
|
23
|
-
|
|
24
10
|
const { isMinimal } = useWorkspaceMode();
|
|
25
|
-
const uniqueScopes = [...new Set(components.map((c) => c.id.scope))];
|
|
26
|
-
const { cloudScopes } = useCloudScopes(uniqueScopes);
|
|
27
|
-
const cloudMap = new Map((cloudScopes || []).map((s) => [s.id.toString(), s]));
|
|
28
|
-
|
|
29
|
-
const compDescriptorMap = new Map(componentDescriptors.map((d) => [d.id.toString(), d]));
|
|
30
|
-
|
|
31
|
-
const items = compact(
|
|
32
|
-
components.map((component) => {
|
|
33
|
-
if (component.deprecation?.isDeprecate) return null;
|
|
34
|
-
|
|
35
|
-
const descriptor = compDescriptorMap.get(component.id.toString());
|
|
36
|
-
if (!descriptor) return null;
|
|
37
|
-
|
|
38
|
-
const cloudScope = cloudMap.get(component.id.scope);
|
|
39
|
-
const scope =
|
|
40
|
-
cloudScope ||
|
|
41
|
-
(ScopeID.isValid(component.id.scope) && { id: ScopeID.fromString(component.id.scope) }) ||
|
|
42
|
-
undefined;
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
component,
|
|
46
|
-
componentDescriptor: descriptor,
|
|
47
|
-
scope: scope
|
|
48
|
-
? { id: scope.id, icon: (scope as any).icon, backgroundIconColor: (scope as any).backgroundIconColor }
|
|
49
|
-
: undefined,
|
|
50
|
-
};
|
|
51
|
-
})
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const [aggregation, setAggregation] = useQueryParamWithDefault<AggregationType>('aggregation', 'namespaces');
|
|
55
|
-
const [activeNamespaces, setActiveNamespaces] = useListParamWithDefault('ns');
|
|
56
|
-
const [activeScopes, setActiveScopes] = useListParamWithDefault('scopes');
|
|
57
|
-
|
|
58
|
-
const filters = useMemo(
|
|
59
|
-
() => ({ namespaces: activeNamespaces, scopes: activeScopes, statuses: new Set() as any }),
|
|
60
|
-
[activeNamespaces, activeScopes]
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const { groups, groupType, availableAggregations, filteredCount } = useWorkspaceAggregation(
|
|
64
|
-
items,
|
|
65
|
-
aggregation,
|
|
66
|
-
filters
|
|
67
|
-
);
|
|
68
11
|
|
|
69
12
|
if (!components.length) return <WorkspaceBlankState />;
|
|
70
13
|
|
|
71
14
|
return (
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
onNamespacesChange={setActiveNamespaces}
|
|
80
|
-
activeScopes={activeScopes}
|
|
81
|
-
onScopesChange={setActiveScopes}
|
|
82
|
-
/>
|
|
83
|
-
|
|
84
|
-
<div className={styles.content}>
|
|
85
|
-
{filteredCount === 0 && <WorkspaceBlankState />}
|
|
86
|
-
|
|
87
|
-
{groups.map((group) => (
|
|
88
|
-
<section key={group.name} className={styles.section}>
|
|
89
|
-
{groupType !== 'none' && (
|
|
90
|
-
<div className={styles.sectionHeader}>
|
|
91
|
-
<NamespaceHeader
|
|
92
|
-
namespace={group.name}
|
|
93
|
-
items={group.items}
|
|
94
|
-
scopeIcon={group.scopeIcon}
|
|
95
|
-
scopeIconColor={group.scopeIconColor}
|
|
96
|
-
/>
|
|
97
|
-
</div>
|
|
98
|
-
)}
|
|
99
|
-
|
|
100
|
-
<ComponentGrid className={styles.cardGrid}>
|
|
101
|
-
{group.items.map((item) => (
|
|
102
|
-
<HopeComponentCard
|
|
103
|
-
key={item.component.id.toString()}
|
|
104
|
-
component={item.component}
|
|
105
|
-
componentDescriptor={item.componentDescriptor}
|
|
106
|
-
scope={item.scope as any}
|
|
107
|
-
showPreview={isMinimal}
|
|
108
|
-
/>
|
|
109
|
-
))}
|
|
110
|
-
</ComponentGrid>
|
|
111
|
-
</section>
|
|
112
|
-
))}
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
15
|
+
<ComponentsOverview
|
|
16
|
+
components={components}
|
|
17
|
+
componentDescriptors={componentDescriptors}
|
|
18
|
+
showPreview={isMinimal}
|
|
19
|
+
storageNamespace="workspace-overview"
|
|
20
|
+
emptyState={<WorkspaceBlankState />}
|
|
21
|
+
/>
|
|
115
22
|
);
|
|
116
23
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
.wrapper {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
height: 100vh;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
background: var(--background-color, #fff);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* ---- Top bar ---- */
|
|
10
|
+
|
|
11
|
+
.topbar {
|
|
12
|
+
flex: none;
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
height: 48px;
|
|
16
|
+
border-bottom: 1px solid var(--border-medium-color, #e6e6e6);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.corner {
|
|
20
|
+
width: 246px;
|
|
21
|
+
height: 100%;
|
|
22
|
+
flex: none;
|
|
23
|
+
border-right: 1px solid var(--border-medium-color, #e6e6e6);
|
|
24
|
+
background: color-mix(in srgb, var(--on-background-color, #000) 3%, transparent);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.topbarNav {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 18px;
|
|
31
|
+
padding: 0 24px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.navItem {
|
|
35
|
+
width: 64px;
|
|
36
|
+
height: 12px;
|
|
37
|
+
border-radius: 4px;
|
|
38
|
+
background: var(--surface01-color, color-mix(in srgb, var(--on-background-color, #000) 6%, transparent));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ---- Main: sidebar + content ---- */
|
|
42
|
+
|
|
43
|
+
.main {
|
|
44
|
+
flex: 1 1;
|
|
45
|
+
display: flex;
|
|
46
|
+
min-height: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.sidebar {
|
|
50
|
+
width: 246px;
|
|
51
|
+
flex: none;
|
|
52
|
+
border-right: 1px solid var(--border-medium-color, #e6e6e6);
|
|
53
|
+
padding: 16px 12px;
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: 14px;
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.sidebarSearch {
|
|
61
|
+
height: 30px;
|
|
62
|
+
border-radius: 8px;
|
|
63
|
+
background: var(--surface01-color, color-mix(in srgb, var(--on-background-color, #000) 6%, transparent));
|
|
64
|
+
margin-bottom: 6px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.sidebarRow {
|
|
68
|
+
height: 11px;
|
|
69
|
+
border-radius: 4px;
|
|
70
|
+
background: var(--surface01-color, color-mix(in srgb, var(--on-background-color, #000) 6%, transparent));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.content {
|
|
74
|
+
flex: 1 1;
|
|
75
|
+
min-width: 0;
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* gentle pulse across the whole shell so it reads as loading */
|
|
80
|
+
.sidebar,
|
|
81
|
+
.topbarNav,
|
|
82
|
+
.corner {
|
|
83
|
+
animation: pulse 1.6s ease-in-out infinite;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@keyframes pulse {
|
|
87
|
+
0%,
|
|
88
|
+
100% {
|
|
89
|
+
opacity: 1;
|
|
90
|
+
}
|
|
91
|
+
50% {
|
|
92
|
+
opacity: 0.65;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@media (prefers-reduced-motion: reduce) {
|
|
97
|
+
.sidebar,
|
|
98
|
+
.topbarNav,
|
|
99
|
+
.corner {
|
|
100
|
+
animation: none;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ComponentsOverviewSkeleton } from '@teambit/explorer.ui.components-overview';
|
|
3
|
+
import styles from './workspace-skeleton.module.scss';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Full-shell loading placeholder shown while the (light) workspace query resolves. Mirrors the real
|
|
7
|
+
* workspace chrome — top bar, sidebar column, content grid — so there is no blank flash and no
|
|
8
|
+
* layout shift when the live UI takes over. Previously this gap rendered an empty `<div>`, which
|
|
9
|
+
* read as a frozen/blank screen on slower workspaces.
|
|
10
|
+
*/
|
|
11
|
+
export function WorkspaceSkeleton() {
|
|
12
|
+
return (
|
|
13
|
+
<div className={styles.wrapper} aria-busy="true" aria-live="polite">
|
|
14
|
+
<div className={styles.topbar}>
|
|
15
|
+
<div className={styles.corner} />
|
|
16
|
+
<div className={styles.topbarNav}>
|
|
17
|
+
<span className={styles.navItem} />
|
|
18
|
+
<span className={styles.navItem} />
|
|
19
|
+
<span className={styles.navItem} />
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div className={styles.main}>
|
|
23
|
+
<div className={styles.sidebar}>
|
|
24
|
+
<span className={styles.sidebarSearch} />
|
|
25
|
+
{Array.from({ length: 10 }).map((_, i) => (
|
|
26
|
+
<span key={i} className={styles.sidebarRow} style={{ width: `${55 + ((i * 13) % 35)}%` }} />
|
|
27
|
+
))}
|
|
28
|
+
</div>
|
|
29
|
+
<div className={styles.content}>
|
|
30
|
+
<ComponentsOverviewSkeleton count={12} />
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -24,6 +24,7 @@ import { useNavigationMessageListener } from '@teambit/workspace.hooks.use-navig
|
|
|
24
24
|
import { useWorkspace } from './use-workspace';
|
|
25
25
|
import { WorkspaceOverview } from './workspace-overview';
|
|
26
26
|
import { WorkspaceProvider } from './workspace-provider';
|
|
27
|
+
import { WorkspaceSkeleton } from './workspace-skeleton';
|
|
27
28
|
import styles from './workspace.module.scss';
|
|
28
29
|
import type { WorkspaceUI } from '../../workspace.ui.runtime';
|
|
29
30
|
import { ThemeFromUrlSync } from './theme-from-url';
|
|
@@ -67,7 +68,11 @@ export function Workspace({ routeSlot, menuSlot, sidebar, workspaceUI, onSidebar
|
|
|
67
68
|
}, []);
|
|
68
69
|
const sidebarOpenness = isSidebarOpen ? Layout.row : Layout.right;
|
|
69
70
|
const themeName = currentTheme?.themeName || 'light';
|
|
70
|
-
|
|
71
|
+
// register the toggler with the parent in an effect, not during render — calling a parent callback
|
|
72
|
+
// (which stores/setStates it) mid-render risks "cannot update a component while rendering another".
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
onSidebarTogglerChange(handleSidebarToggle);
|
|
75
|
+
}, [onSidebarTogglerChange, handleSidebarToggle]);
|
|
71
76
|
|
|
72
77
|
useEffect(() => {
|
|
73
78
|
if (!window) return;
|
|
@@ -82,14 +87,20 @@ export function Workspace({ routeSlot, menuSlot, sidebar, workspaceUI, onSidebar
|
|
|
82
87
|
|
|
83
88
|
const location = useLocation();
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
// push the loaded components into the workspace UI store from an effect rather than during render —
|
|
91
|
+
// mutating an external store mid-render can re-render its subscribers in the middle of this render.
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (workspace) workspaceUI.setComponents(workspace.components);
|
|
94
|
+
}, [workspaceUI, workspace]);
|
|
88
95
|
|
|
89
|
-
workspaceUI.setComponents(workspace.components);
|
|
90
96
|
const inIframe = typeof window !== 'undefined' && window.parent && window.parent !== window;
|
|
91
97
|
const isOverview = location.pathname === '/' || location.pathname === '';
|
|
92
98
|
const showTopBar = !isMinimal || (isMinimal && !isOverview);
|
|
99
|
+
if (!workspace) {
|
|
100
|
+
// While the light workspace query resolves, show a full-shell skeleton instead of a blank
|
|
101
|
+
// screen. The heavy per-component data (status, issues) streams in after first paint.
|
|
102
|
+
return <WorkspaceSkeleton />;
|
|
103
|
+
}
|
|
93
104
|
|
|
94
105
|
return (
|
|
95
106
|
<WorkspaceProvider workspace={workspace}>
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
export declare function ChangedPill(): React.JSX.Element;
|
|
3
|
-
export declare function BuildSpinner(): React.JSX.Element;
|
|
4
|
-
export declare function BuildingPreview(): React.JSX.Element;
|
|
5
|
-
export declare function QueuedPreview(): React.JSX.Element;
|