@camunda/camunda-composite-components 0.17.1 → 0.18.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/README.md CHANGED
@@ -67,6 +67,54 @@ return (
67
67
  )
68
68
  ```
69
69
 
70
+ Opt into SSE by using the `C3ClusterUpdateManager` Provider
71
+
72
+ ```tsx
73
+ // App.tsx
74
+ import { C3ClusterUpdateManager } from "@camunda/camunda-composite-components"
75
+
76
+ return (
77
+ <C3ClusterUpdateManager stage={getEnv()} userToken={auth.token!}>
78
+ <App />
79
+ </C3ClusterUpdateManager>
80
+ )
81
+ ```
82
+
83
+ Listen for SSE events from anywhere in your app
84
+
85
+ ```tsx
86
+ // ClusterPage.tsx
87
+
88
+ useEffect(() => {
89
+ const handler = (event) => {
90
+ const data = event.detail
91
+
92
+ if (data && data.org && data.org === currentOrgId) {
93
+ switch (data.entity) {
94
+ case "cluster": {
95
+ }
96
+ case "cluster-client": {
97
+ }
98
+ case "alert-subscription": {
99
+ }
100
+ case "connector-secrets": {
101
+ }
102
+ case "console-api-client": {
103
+ }
104
+ case "backup": {
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ window.addEventListener("clusterchange", handler)
111
+
112
+ return () => {
113
+ window.removeEventListener("clusterchange", handler)
114
+ }
115
+ }, [])
116
+ ```
117
+
70
118
  ## <a name="c4list"></a> (incomplete) List of adopters of C3+C4
71
119
 
72
120
  - [Console team](https://confluence.camunda.com/display/HAN/Console+Team)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/camunda-composite-components",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -0,0 +1,10 @@
1
+ import { EventSourcePolyfill } from "event-source-polyfill";
2
+ import { Endpoints, Stage } from "./endpoints.const";
3
+ export interface C3ClustersProps {
4
+ stage?: Stage;
5
+ endpoints?: Endpoints;
6
+ userToken: string;
7
+ }
8
+ export declare class ClustersService {
9
+ static clustersStream(options: C3ClustersProps): EventSourcePolyfill;
10
+ }
@@ -0,0 +1,27 @@
1
+ import { EventSourcePolyfill } from "event-source-polyfill";
2
+ import { CONSOLE, getEndpointByOptions, } from "./endpoints.const";
3
+ export class ClustersService {
4
+ static clustersStream(options) {
5
+ const baseUrl = getEndpointByOptions({
6
+ ...options,
7
+ endpoint: CONSOLE,
8
+ });
9
+ let source = new EventSourcePolyfill(`${baseUrl}/external/events`, {
10
+ headers: {
11
+ authorization: `Bearer ${options.userToken}`,
12
+ },
13
+ heartbeatTimeout: 2 * 60 * 1000,
14
+ });
15
+ source.onmessage = function (event) {
16
+ const data = JSON.parse(event.data);
17
+ const clusterChangeEvent = new CustomEvent("clusterchange", {
18
+ detail: data,
19
+ });
20
+ window.dispatchEvent(clusterChangeEvent);
21
+ };
22
+ source.onerror = (_error) => {
23
+ console.log({ _error });
24
+ };
25
+ return source;
26
+ }
27
+ }
@@ -7,9 +7,11 @@ export interface Endpoint {
7
7
  export interface Endpoints {
8
8
  notifications?: string;
9
9
  accounts?: string;
10
+ console?: string;
10
11
  }
11
12
  export declare const NOTIFICATIONS: Endpoint;
12
13
  export declare const ACCOUNTS: Endpoint;
14
+ export declare const CONSOLE: Endpoint;
13
15
  export type Stage = "dev" | "int" | "prod";
14
16
  export declare function getEndpoint(stage: Stage, endpoint: Endpoint): string;
15
17
  export declare function getEndpointByOptions(options: {
@@ -1,14 +1,20 @@
1
1
  export const NOTIFICATIONS = {
2
2
  id: "notifications",
3
3
  dev: "https://notifications.cloud.dev.ultrawombat.com",
4
- int: "https://notifications.cloud.ultrawombat.com",
5
- prod: "https://notifications.cloud.camunda.io",
4
+ int: "https://notifications.ultrawombat.com",
5
+ prod: "https://notifications.camunda.io",
6
6
  };
7
7
  export const ACCOUNTS = {
8
8
  id: "accounts",
9
9
  dev: "https://accounts.cloud.dev.ultrawombat.com",
10
- int: "https://accounts.cloud.ultrawombat.com",
11
- prod: "https://accounts.cloud.camunda.io",
10
+ int: "https://accounts.ultrawombat.com",
11
+ prod: "https://accounts.camunda.io",
12
+ };
13
+ export const CONSOLE = {
14
+ id: "console",
15
+ dev: "https://console.cloud.dev.ultrawombat.com",
16
+ int: "https://console.ultrawombat.com",
17
+ prod: "https://console.camunda.io",
12
18
  };
13
19
  export function getEndpoint(stage, endpoint) {
14
20
  switch (stage) {
@@ -39,6 +45,13 @@ export function getEndpointByOptions(options) {
39
45
  else if (options.stage) {
40
46
  return getEndpoint(options.stage, options.endpoint);
41
47
  }
48
+ case "console":
49
+ if (options.endpoints?.console) {
50
+ return options.endpoints.console;
51
+ }
52
+ else if (options.stage) {
53
+ return getEndpoint(options.stage, options.endpoint);
54
+ }
42
55
  }
43
56
  throw new Error(`Missing stage or notifications endpoint`);
44
57
  }
@@ -8,6 +8,7 @@ import { useC3Profile } from "../../c3-user-configuration/c3-profile-provider/c3
8
8
  import { useC3UserConfiguration } from "../../c3-user-configuration/c3-user-configuration-provider";
9
9
  import { APPS } from "../../../utils/camunda";
10
10
  import { NavWrapper, SideNav } from "./components";
11
+ import { useClusterUpdate } from "../../../contexts/c3-cluster-update-manager";
11
12
  const getOrgLink = (app, orgId, domain) => {
12
13
  switch (app) {
13
14
  case "console": {
@@ -27,7 +28,7 @@ const getClustersToRender = (clusters, activeOrg, appToNavigateTo) => {
27
28
  // either the user has appreadpermissions on this app
28
29
  (activeOrg?.permissions?.cluster?.[appToNavigateTo]?.read ||
29
30
  // OR this is a 8.8+ cluster - in this case we also allow rendering this item
30
- [1, 2, 3, 4, 5, 6, 7].every((pre8Minor) => !generation.name.includes(`8.${pre8Minor}`))));
31
+ [1, 2, 3, 4, 5, 6, 7].every((pre8Minor) => !generation.versions?.zeebe?.includes(`8.${pre8Minor}`))));
31
32
  };
32
33
  const OrgPickerModal = ({ isOpen, onCancel, orgs, activeOrg, loadingStatus, appToNavigateTo, domain, }) => {
33
34
  const hasNoTeams = Object.values(orgs ?? {})?.every(({ org }) => {
@@ -73,15 +74,94 @@ const SubElementWrapper = styled.ul `
73
74
  visibility: inherit;
74
75
  }
75
76
  `;
76
- export const C3NavigationAppBar = ({ app: appProps, appBar, forwardRef, navbar, }) => {
77
+ export const C3NavigationAppBar = ({ app: appProps, appBar, forwardRef, navbar, toggleAppbar, }) => {
77
78
  const { currentApp, domain, analyticsTrack, currentClusterUuid } = useC3UserConfiguration();
78
- const { orgs, clusters, activeOrg, isEnabled, loadClustersById } = useC3Profile();
79
- const [appBarOpen, setAppBarOpen] = useState(appBar.isOpen || false);
80
- const { setPanelRef: panelRef, setIconRef: iconRef } = useOnClickOutside(() => setAppBarOpen(false));
79
+ const { orgs, clusters, setClusters, activeOrg, isEnabled, loadClustersById, } = useC3Profile();
80
+ const { setPanelRef: panelRef, setIconRef: iconRef } = useOnClickOutside(() => toggleAppbar(false));
81
81
  const [appElements, setAppElements] = useState([]);
82
82
  const [loadingStatus, setLoadingStatus] = useState("inactive");
83
83
  const [clustersByOrgId, setClustersByOrgId] = useState(null);
84
84
  const [appToNavigateTo, setAppToNavigateTo] = useState(APPS[0]);
85
+ useClusterUpdate({
86
+ onUpdate: (data) => {
87
+ if (data && data.org && data.org === activeOrg?.uuid) {
88
+ switch (data.entity) {
89
+ case "cluster": {
90
+ if (data.payload.uuid) {
91
+ const clusterDto = data.payload;
92
+ const status = clusterDto.status;
93
+ const endpoints = {
94
+ tasklist: status?.tasklistUrl,
95
+ operate: status?.operateUrl,
96
+ optimize: status?.optimizeUrl,
97
+ console: "",
98
+ modeler: "",
99
+ identity: "",
100
+ };
101
+ const expectedStatus = {
102
+ tasklist: status?.tasklistStatus,
103
+ operate: status?.operateStatus,
104
+ optimize: status?.optimizeStatus,
105
+ console: "",
106
+ modeler: "",
107
+ identity: "",
108
+ };
109
+ const affectedCluster = {
110
+ uuid: clusterDto.uuid,
111
+ name: clusterDto.name,
112
+ status: expectedStatus,
113
+ generation: clusterDto.generation,
114
+ endpoints,
115
+ };
116
+ switch (data.type) {
117
+ case "deleted": {
118
+ setClusters((clusters) => clusters?.filter((cluster) => cluster.uuid !== affectedCluster.uuid) ?? []);
119
+ break;
120
+ }
121
+ case "updated": {
122
+ setClusters((clusters) => {
123
+ if (!clusters) {
124
+ return [];
125
+ }
126
+ const clustersClone = [...clusters];
127
+ const clusterToUpdateIndex = clustersClone?.findIndex((cluster) => cluster.uuid === affectedCluster.uuid);
128
+ if (clusterToUpdateIndex > -1) {
129
+ clustersClone[clusterToUpdateIndex] = affectedCluster;
130
+ return clustersClone;
131
+ }
132
+ return clusters;
133
+ });
134
+ }
135
+ case "created": {
136
+ setClusters((clusters) => {
137
+ if (!clusters) {
138
+ return [];
139
+ }
140
+ const clustersClone = [...clusters];
141
+ const clusterToUpdateIndex = clustersClone?.findIndex((cluster) => cluster.uuid === affectedCluster.uuid);
142
+ if (clusterToUpdateIndex > -1) {
143
+ clustersClone[clusterToUpdateIndex] = affectedCluster;
144
+ }
145
+ else {
146
+ clustersClone.push(affectedCluster);
147
+ }
148
+ return clustersClone;
149
+ });
150
+ break;
151
+ }
152
+ default:
153
+ break;
154
+ }
155
+ }
156
+ break;
157
+ }
158
+ default: {
159
+ break;
160
+ }
161
+ }
162
+ }
163
+ },
164
+ });
85
165
  if (!appBar.elements && !isEnabled)
86
166
  console.warn("No app elements and user config provided. Please provide at least one of them.");
87
167
  const isMemberOfTeam = useCallback((orgUuid) => orgs?.some(({ uuid }) => uuid === orgUuid) || false, [orgs]);
@@ -165,7 +245,7 @@ export const C3NavigationAppBar = ({ app: appProps, appBar, forwardRef, navbar,
165
245
  label: cluster.name,
166
246
  href: cluster.endpoints[app],
167
247
  onClick: () => {
168
- setAppBarOpen(false);
248
+ toggleAppbar(false);
169
249
  analyticsTrack?.(`${app}:open`, { currentApp: app });
170
250
  },
171
251
  isActive: currentApp === app && currentClusterUuid === cluster.uuid,
@@ -199,7 +279,7 @@ export const C3NavigationAppBar = ({ app: appProps, appBar, forwardRef, navbar,
199
279
  }
200
280
  }
201
281
  setAppElements(defaultElements);
202
- }, [clusters, currentApp, domain, handleSuperOrg, getClustersForSuperOrg]);
282
+ }, [clusters, currentApp, domain, handleSuperOrg]);
203
283
  const appBarElements = appBar.elements || (clusters || clustersByOrgId ? appElements : null);
204
284
  const [isOrgPickerModalOpen, setIsOrgPickerModalOpen] = useState(false);
205
285
  return (React.createElement(React.Fragment, null,
@@ -209,28 +289,28 @@ export const C3NavigationAppBar = ({ app: appProps, appBar, forwardRef, navbar,
209
289
  , {
210
290
  // eslint-disable-next-line
211
291
  // @ts-ignore
212
- ref: iconRef, "aria-label": "Camunda components", isActive: appBarOpen, onClick: () => {
213
- setAppBarOpen(!appBarOpen);
292
+ ref: iconRef, "aria-label": "Camunda components", isActive: appBar.isOpen, onClick: () => {
293
+ toggleAppbar(!appBar.isOpen);
214
294
  }, tooltipAlignment: "start",
215
295
  /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
216
296
  /* @ts-ignore */
217
- leaveDelayMs: 100 }, appBarOpen ? React.createElement(Close, { size: 20 }) : React.createElement(C3AppMenuIcon, { size: 20 })),
297
+ leaveDelayMs: 100 }, appBar.isOpen ? React.createElement(Close, { size: 20 }) : React.createElement(C3AppMenuIcon, { size: 20 })),
218
298
  React.createElement(OrgPickerModal, { activeOrg: activeOrg, appToNavigateTo: appToNavigateTo, isOpen: isOrgPickerModalOpen, orgs: clustersByOrgId, onCancel: () => {
219
299
  setIsOrgPickerModalOpen(false);
220
300
  }, loadingStatus: loadingStatus, domain: domain }),
221
301
  React.createElement(NavWrapper, null,
222
- React.createElement(SideNav, { ref: panelRef, "aria-label": appBar.ariaLabel || "Side Navigation", expanded: appBarOpen, isPersistent: false },
302
+ React.createElement(SideNav, { ref: panelRef, "aria-label": appBar.ariaLabel || "Side Navigation", expanded: appBar.isOpen, isPersistent: false },
223
303
  React.createElement(SideNavItems, null,
224
304
  React.createElement("li", null, navbar.elements.length > 0 && (React.createElement(HeaderSideNavItems, { hasDivider: true }, navbar.elements.map((element) => (React.createElement(HeaderMenuItem, { key: element.key, as: element.routeProps && forwardRef, isActive: element.isCurrentPage, ...element.routeProps, onClick: () => {
225
305
  if (element.routeProps.onClick) {
226
306
  element.routeProps.onClick();
227
307
  }
228
308
  if (appBar.closeOnClick !== false) {
229
- setAppBarOpen(false);
309
+ toggleAppbar(false);
230
310
  }
231
311
  } }, element.label)))))),
232
312
  React.createElement("li", null,
233
- React.createElement(SubElementWrapper, { "$expanded": appBarOpen }, appBarElements &&
313
+ React.createElement(SubElementWrapper, { "$expanded": appBar.isOpen }, appBarElements &&
234
314
  appBarElements.map((element) => {
235
315
  if (element.subElements && element.subElements.length > 0) {
236
316
  return (React.createElement(SideNavMenu, { large: true, title: element.label, key: element.key }, element.subElements.map((subElement) => (React.createElement(SideNavMenuItem, { as: subElement.routeProps && forwardRef, key: subElement.key, href: subElement.href, target: subElement.href ? subElement.target : undefined, isActive: subElement.isActive || subElement.active, ...subElement.routeProps, onClick: () => {
@@ -241,7 +321,7 @@ export const C3NavigationAppBar = ({ app: appProps, appBar, forwardRef, navbar,
241
321
  subElement.routeProps.onClick();
242
322
  }
243
323
  if (appBar.closeOnClick !== false) {
244
- setAppBarOpen(false);
324
+ toggleAppbar(false);
245
325
  }
246
326
  if (appBar.elementClicked) {
247
327
  appBar.elementClicked(subElement.key);
@@ -257,7 +337,7 @@ export const C3NavigationAppBar = ({ app: appProps, appBar, forwardRef, navbar,
257
337
  element.routeProps.onClick();
258
338
  }
259
339
  if (appBar.closeOnClick !== false) {
260
- setAppBarOpen(false);
340
+ toggleAppbar(false);
261
341
  }
262
342
  if (appBar.elementClicked) {
263
343
  appBar.elementClicked(element.key);
@@ -1,5 +1,5 @@
1
- import { Header, HeaderContainer, HeaderGlobalBar, HeaderMenuItem, HeaderName, HeaderNavigation, SkipToContent, Tag, Toggletip, ToggletipButton, ToggletipContent, Stack as CarbonStack, Link, } from "@carbon/react";
2
- import React from "react";
1
+ import { Header, HeaderGlobalBar, HeaderMenuItem, HeaderName, HeaderNavigation, SkipToContent, Tag, Toggletip, ToggletipButton, ToggletipContent, Stack as CarbonStack, Link, } from "@carbon/react";
2
+ import React, { useState } from "react";
3
3
  import C3InfoSidebar from "./c3-navigation-sidebar/c3-info-sidebar";
4
4
  import { C3NotificationSidebar } from "./c3-navigation-sidebar/c3-notification-sidebar";
5
5
  import C3OrgSidebar from "./c3-org-sidebar/c3-org-sidebar";
@@ -56,9 +56,10 @@ export const C3Navigation = ({ app, appBar, forwardRef, navbar, orgSideBar, info
56
56
  const { activeOrg } = useC3Profile();
57
57
  const isLargeScreen = useMediaQuery(`(min-width: ${BREAKPOINT_LG_WIDTH}`);
58
58
  const appBarElementsLength = appBar.elements?.length ?? 0;
59
- const displayAppBar = appBarElementsLength > 0 ||
59
+ const displayAppBar = !!(appBarElementsLength > 0 ||
60
60
  appBar.appTeaserRouteProps ||
61
- (!isLargeScreen && navbar.elements.length > 0);
61
+ (!isLargeScreen && navbar.elements.length > 0));
62
+ const [appBarOpen, setAppBarOpen] = useState(appBar.isOpen || false);
62
63
  const orgName = activeOrg?.name || navbar.orgName;
63
64
  let expiresAt;
64
65
  if (navbar.licenseTag?.expiresAt !== undefined) {
@@ -146,69 +147,69 @@ export const C3Navigation = ({ app, appBar, forwardRef, navbar, orgSideBar, info
146
147
  if (app.prefix)
147
148
  console.warn("The `prefix` prop is deprecated and will be removed in a future release. It has been replaced with a Camunda icon.");
148
149
  return (React.createElement(C3SidebarStateProvider, { isNotificationSidebarOpen: notificationSideBar?.isOpen, isOrgSidebarOpen: orgSideBar?.isOpen, isInfoSidebarOpen: infoSideBar?.isOpen, isUserSidebarOpen: userSideBar?.isOpen },
149
- React.createElement(HeaderContainer, { render: () => {
150
- return (React.createElement(Header, { "aria-label": app.ariaLabel },
151
- React.createElement(SkipToContent, null),
152
- displayAppBar && (React.createElement(C3NavigationAppBar, { app: app, appBar: appBar, forwardRef: forwardRef, navbar: navbar })),
153
- React.createElement(HeaderName, { as: forwardRef, prefix: "", ...app.routeProps },
154
- React.createElement(Stack, { gap: 3, orientation: "horizontal" },
155
- React.createElement(CamundaLogo, { "aria-label": "Camunda" }),
156
- React.createElement("span", null, app.name))),
157
- React.createElement(HeaderNavigation, { "aria-label": app.ariaLabel }, navbar.elements.map((element) => (React.createElement(HeaderMenuItem, { key: element.key, as: element.routeProps && forwardRef, isActive: element.isCurrentPage, ...element.routeProps },
158
- React.createElement("span", null, element.label))))),
159
- React.createElement(HeaderGlobalBar, null,
160
- React.createElement("div", { style: {
161
- display: "grid",
162
- gridAutoFlow: "column",
163
- gap: ".5rem",
164
- paddingRight: ".5rem",
165
- } },
166
- tags &&
167
- tags.length > 0 &&
168
- tags.map((tag) => {
169
- if (tag?.tooltip !== undefined) {
170
- const { content, buttonLabel } = tag.tooltip;
171
- return (React.createElement("div", { key: tag.key, style: {
172
- height: "1.5rem",
173
- marginTop: "0.75rem",
174
- } },
175
- React.createElement(Toggletip, null,
176
- React.createElement(ToggletipButton, { as: "span", label: buttonLabel },
177
- React.createElement(Tag, { type: tag.color, renderIcon: tag.renderIcon, style: {
178
- margin: "0",
179
- cursor: "pointer",
180
- overflow: "hidden",
181
- whiteSpace: "nowrap",
182
- textOverflow: "ellipsis",
183
- } }, tag.label)),
184
- React.createElement(StyledToggletipContent, null, content))));
185
- }
186
- return (React.createElement(Tag, { key: tag.key, renderIcon: tag.renderIcon, style: {
187
- height: "1.5rem",
188
- marginTop: "0.75rem",
189
- overflow: "hidden",
190
- whiteSpace: "nowrap",
191
- textOverflow: "ellipsis",
192
- }, type: tag.color }, tag.label));
193
- }),
194
- (clusterUuid || currentClusterUuid) && (React.createElement(ClusterTagWrapper, null,
195
- React.createElement(C3ClusterTagWithClusterName, { clusterUuid: clusterUuid || currentClusterUuid || "", conditionalRendering: options?.conditionalTagRendering }))),
196
- orgName && (React.createElement(OrgName, { orgName: orgName, isSuperOrg: activeOrg?.isSuperOrg, activeSuperOrg: activeOrg?.superOrganization?.name }))),
197
- actionButtons && React.createElement(C3ActionButtons, { elements: actionButtons }),
198
- notificationSideBar && (React.createElement(C3NotificationSidebar, { sideBar: {
199
- ...notificationSideBar,
200
- type: "notifications",
201
- } })),
202
- orgSideBar && (React.createElement(C3OrgSidebar, { sideBar: {
203
- ...orgSideBar,
204
- type: "org",
205
- } })),
206
- infoButton || helpCenter ? (React.createElement(HelpCenterHint, null,
207
- React.createElement(InfoButton, { onClick: infoButton ? infoButton.onClick : () => openHelpCenter() }))) : (infoSideBar && (React.createElement(C3InfoSidebar, { sideBar: { ...infoSideBar, type: "info" } }))),
208
- userSideBar && (React.createElement(C3UserSidebar, { sideBar: {
209
- ...userSideBar,
210
- type: "user",
211
- } })))));
212
- } }),
150
+ React.createElement(Header, { "aria-label": app.ariaLabel },
151
+ React.createElement(SkipToContent, null),
152
+ displayAppBar && (React.createElement(C3NavigationAppBar, { app: app, appBar: { ...appBar, isOpen: appBarOpen }, forwardRef: forwardRef, navbar: navbar, toggleAppbar: (value) => {
153
+ setAppBarOpen(value);
154
+ } })),
155
+ React.createElement(HeaderName, { as: forwardRef, prefix: "", ...app.routeProps },
156
+ React.createElement(Stack, { gap: 3, orientation: "horizontal" },
157
+ React.createElement(CamundaLogo, { "aria-label": "Camunda" }),
158
+ React.createElement("span", null, app.name))),
159
+ React.createElement(HeaderNavigation, { "aria-label": app.ariaLabel }, navbar.elements.map((element) => (React.createElement(HeaderMenuItem, { key: element.key, as: element.routeProps && forwardRef, isActive: element.isCurrentPage, ...element.routeProps },
160
+ React.createElement("span", null, element.label))))),
161
+ React.createElement(HeaderGlobalBar, null,
162
+ React.createElement("div", { style: {
163
+ display: "grid",
164
+ gridAutoFlow: "column",
165
+ gap: ".5rem",
166
+ paddingRight: ".5rem",
167
+ } },
168
+ tags &&
169
+ tags.length > 0 &&
170
+ tags.map((tag) => {
171
+ if (tag?.tooltip !== undefined) {
172
+ const { content, buttonLabel } = tag.tooltip;
173
+ return (React.createElement("div", { key: tag.key, style: {
174
+ height: "1.5rem",
175
+ marginTop: "0.75rem",
176
+ } },
177
+ React.createElement(Toggletip, null,
178
+ React.createElement(ToggletipButton, { as: "span", label: buttonLabel },
179
+ React.createElement(Tag, { type: tag.color, renderIcon: tag.renderIcon, style: {
180
+ margin: "0",
181
+ cursor: "pointer",
182
+ overflow: "hidden",
183
+ whiteSpace: "nowrap",
184
+ textOverflow: "ellipsis",
185
+ } }, tag.label)),
186
+ React.createElement(StyledToggletipContent, null, content))));
187
+ }
188
+ return (React.createElement(Tag, { key: tag.key, renderIcon: tag.renderIcon, style: {
189
+ height: "1.5rem",
190
+ marginTop: "0.75rem",
191
+ overflow: "hidden",
192
+ whiteSpace: "nowrap",
193
+ textOverflow: "ellipsis",
194
+ }, type: tag.color }, tag.label));
195
+ }),
196
+ (clusterUuid || currentClusterUuid) && (React.createElement(ClusterTagWrapper, null,
197
+ React.createElement(C3ClusterTagWithClusterName, { clusterUuid: clusterUuid || currentClusterUuid || "", conditionalRendering: options?.conditionalTagRendering }))),
198
+ orgName && (React.createElement(OrgName, { orgName: orgName, isSuperOrg: activeOrg?.isSuperOrg, activeSuperOrg: activeOrg?.superOrganization?.name }))),
199
+ actionButtons && React.createElement(C3ActionButtons, { elements: actionButtons }),
200
+ notificationSideBar && (React.createElement(C3NotificationSidebar, { sideBar: {
201
+ ...notificationSideBar,
202
+ type: "notifications",
203
+ } })),
204
+ orgSideBar && (React.createElement(C3OrgSidebar, { sideBar: {
205
+ ...orgSideBar,
206
+ type: "org",
207
+ } })),
208
+ infoButton || helpCenter ? (React.createElement(HelpCenterHint, null,
209
+ React.createElement(InfoButton, { onClick: infoButton ? infoButton.onClick : () => openHelpCenter() }))) : (infoSideBar && (React.createElement(C3InfoSidebar, { sideBar: { ...infoSideBar, type: "info" } }))),
210
+ userSideBar && (React.createElement(C3UserSidebar, { sideBar: {
211
+ ...userSideBar,
212
+ type: "user",
213
+ } })))),
213
214
  helpCenter && React.createElement(C3HelpCenter, { ...helpCenter })));
214
215
  };
@@ -80,6 +80,7 @@ export interface C3NavigationProps {
80
80
  options?: {
81
81
  conditionalTagRendering?: (stage: CamundaClusterStage) => boolean;
82
82
  };
83
+ toggleAppbar: (value: boolean) => void;
83
84
  }
84
85
  export type LinkProps = {
85
86
  element?: React.ElementType;
@@ -2,36 +2,34 @@ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  export function useOnClickOutside(handler) {
3
3
  const panelRef = useRef(null);
4
4
  const iconRef = useRef(null);
5
- const listener = (event) => {
6
- if (!panelRef.current ||
7
- !iconRef.current ||
8
- (event.target instanceof Node &&
9
- (panelRef.current.contains(event.target) ||
10
- iconRef.current.contains(event.target)))) {
11
- return;
12
- }
13
- handler(event);
14
- };
15
- const setListener = () => {
5
+ const handlerRef = useRef(handler);
6
+ handlerRef.current = handler;
7
+ useEffect(() => {
8
+ const listener = (event) => {
9
+ if (!panelRef.current ||
10
+ !iconRef.current ||
11
+ (event.target instanceof Node &&
12
+ (panelRef.current.contains(event.target) ||
13
+ iconRef.current.contains(event.target)))) {
14
+ return;
15
+ }
16
+ handlerRef.current(event);
17
+ };
16
18
  document.addEventListener("mousedown", listener);
17
19
  document.addEventListener("touchstart", listener);
18
- };
19
- useEffect(() => {
20
20
  return () => {
21
21
  document.removeEventListener("mousedown", listener);
22
22
  document.removeEventListener("touchstart", listener);
23
23
  };
24
- }, []);
24
+ }, [handlerRef]);
25
25
  const setPanelRef = useCallback((node) => {
26
26
  if (node && node instanceof HTMLElement) {
27
27
  panelRef.current = node;
28
- setListener();
29
28
  }
30
29
  }, []);
31
30
  const setIconRef = useCallback((node) => {
32
31
  if (node && node instanceof HTMLElement) {
33
32
  iconRef.current = node;
34
- setListener();
35
33
  }
36
34
  }, []);
37
35
  return { panelRef, setPanelRef, iconRef, setIconRef };
@@ -13,8 +13,12 @@ export const SuperUserToggle = ({ isActive, onChange }) => {
13
13
  };
14
14
  export const NavbarWithCustomSection = () => {
15
15
  const [isSuperAdminModeActive, setIsSuperAdminModeActive] = useState(true);
16
+ const [appbarOpen, setAppBarOpen] = useState(false);
17
+ const appbarProps = createAppBarProps();
16
18
  return (React.createElement(React.Fragment, null,
17
- React.createElement(C3Navigation, { app: createAppProps(), appBar: createAppBarProps(), navbar: {
19
+ React.createElement(C3Navigation, { app: createAppProps(), appBar: { ...appbarProps, isOpen: appbarProps.isOpen ?? appbarOpen }, toggleAppbar: (value) => {
20
+ setAppBarOpen(value);
21
+ }, navbar: {
18
22
  ...createNavBarBarProps(),
19
23
  tags: isSuperAdminModeActive
20
24
  ? [
@@ -12,6 +12,7 @@ export type C3ProfileContextValue = {
12
12
  reloadOrgs: () => void;
13
13
  removeOrg: (orgId: string) => void;
14
14
  clusters: Cluster[] | null;
15
+ setClusters: React.Dispatch<React.SetStateAction<Cluster[] | null>>;
15
16
  reloadClusters: () => void;
16
17
  resolvedTheme: ResolvedTheme;
17
18
  onThemeChange: (newTheme: Theme) => void;
@@ -16,6 +16,7 @@ export const C3ProfileContext = createContext({
16
16
  resolvedTheme: defaultTheme,
17
17
  onThemeChange: () => undefined,
18
18
  loadClustersById: () => undefined,
19
+ setClusters: () => undefined,
19
20
  });
20
21
  export const C3ProfileProvider = ({ children }) => {
21
22
  const { decodedAudience, analyticsTrack, ...config } = useContext(C3UserConfigurationContext);
@@ -37,7 +38,7 @@ export const C3ProfileProvider = ({ children }) => {
37
38
  queryKey: [decodedAudience, config.userToken],
38
39
  enabled: isConfigValid,
39
40
  });
40
- const { data: clusters, refetch: loadClusters } = useApi({
41
+ const { data: clusters, setData: setClusters, refetch: loadClusters, } = useApi({
41
42
  queryFn: () => {
42
43
  if (!isConfigValid)
43
44
  return;
@@ -99,6 +100,7 @@ export const C3ProfileProvider = ({ children }) => {
99
100
  activeOrg,
100
101
  reloadOrgs: loadOrgs,
101
102
  clusters,
103
+ setClusters,
102
104
  reloadClusters: loadClusters,
103
105
  removeOrg,
104
106
  onThemeChange,
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import { Stage } from "../api/endpoints.const";
3
+ type C3ClusterUpdateManagerProps = {
4
+ children: React.ReactNode;
5
+ stage: Stage;
6
+ userToken: string;
7
+ };
8
+ declare const C3ClusterUpdateManager: ({ children, stage, userToken, }: C3ClusterUpdateManagerProps) => JSX.Element;
9
+ declare const useClusterUpdate: ({ onUpdate }: {
10
+ onUpdate: (data: any) => void;
11
+ }) => void;
12
+ export { C3ClusterUpdateManager, useClusterUpdate };
@@ -0,0 +1,27 @@
1
+ import React, { useRef } from "react";
2
+ import { useEffect, createContext } from "react";
3
+ import { ClustersService } from "../api/clusters";
4
+ const C3ClusterUpdateManagerContext = createContext(null);
5
+ const C3ClusterUpdateManager = ({ children, stage, userToken, }) => {
6
+ useEffect(() => {
7
+ const eventSource = ClustersService.clustersStream({ stage, userToken });
8
+ return () => {
9
+ eventSource.close();
10
+ };
11
+ }, [stage, userToken]);
12
+ return (React.createElement(C3ClusterUpdateManagerContext.Provider, { value: null }, children));
13
+ };
14
+ const useClusterUpdate = ({ onUpdate }) => {
15
+ const onUpdateRef = useRef(onUpdate);
16
+ onUpdateRef.current = onUpdate;
17
+ useEffect(() => {
18
+ const handler = (event) => {
19
+ onUpdateRef.current(event.detail);
20
+ };
21
+ window.addEventListener("clusterchange", handler);
22
+ return () => {
23
+ window.removeEventListener("clusterchange", handler);
24
+ };
25
+ }, [onUpdateRef]);
26
+ };
27
+ export { C3ClusterUpdateManager, useClusterUpdate };
@@ -28,3 +28,4 @@ export { C3DataTable } from "./components/c3-data-table/c3-data-table";
28
28
  export { C3DataTableActionProps, C3DataTableHeaderContentType, C3DataTableHeadersProps, C3DataTableProps, C3DataTableToolbarButtonItemProps, C3DataTableToolbarMultiItemButtonProps, C3DataTableToolbarMultiSelectItemProps, C3DataTableToolbarProps, C3DataTableToolbarSearchItemProps, Filter, RowBaseProps, } from "./components/c3-data-table/c3-data-table.types";
29
29
  export { RequestResponse } from "./api/api";
30
30
  export { useApi } from "./hooks/useApi";
31
+ export { C3ClusterUpdateManager } from "./contexts/c3-cluster-update-manager";
@@ -19,3 +19,4 @@ export * from "./assets/app-teaser-images/tasklist-3-api";
19
19
  export { C3Page } from "./components/c3-page/c3-page";
20
20
  export { C3DataTable } from "./components/c3-data-table/c3-data-table";
21
21
  export { useApi } from "./hooks/useApi";
22
+ export { C3ClusterUpdateManager } from "./contexts/c3-cluster-update-manager";
@@ -50,6 +50,13 @@ export type Cluster = {
50
50
  };
51
51
  generation: {
52
52
  name: string;
53
+ versions: {
54
+ zeebe: string;
55
+ operate: string;
56
+ tasklist: string;
57
+ optimize: string;
58
+ connectors: string;
59
+ };
53
60
  };
54
61
  };
55
62
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/camunda-composite-components",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"