Skip to content

Commit f03aa16

Browse files
committed
feat: implement device group subscriptions
- implement GraphQL subscriptions for device group changes Signed-off-by: Osman Hadzic <osman.hadzic@secomind.com>
1 parent d688228 commit f03aa16

2 files changed

Lines changed: 181 additions & 3 deletions

File tree

frontend/src/api/schema.graphql

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,12 @@ type LocalizedAttribute {
900900
languageTag: String!
901901
}
902902

903+
type base_image_collection_result {
904+
created: BaseImageCollection
905+
updated: BaseImageCollection
906+
destroyed: ID
907+
}
908+
903909
"The result of the :delete_base_image_collection mutation"
904910
type DeleteBaseImageCollectionResult {
905911
"The record that was successfully deleted"
@@ -1092,6 +1098,12 @@ type BaseImageCollection implements Node {
10921098
): BaseImageConnection!
10931099
}
10941100

1101+
type base_image_result {
1102+
created: BaseImage
1103+
updated: BaseImage
1104+
destroyed: ID
1105+
}
1106+
10951107
"The result of the :delete_base_image mutation"
10961108
type DeleteBaseImageResult {
10971109
"The record that was successfully deleted"
@@ -4716,6 +4728,12 @@ type ForwarderConfig {
47164728
secureSessions: Boolean!
47174729
}
47184730

4731+
type device_group_result {
4732+
created: DeviceGroup
4733+
updated: DeviceGroup
4734+
destroyed: ID
4735+
}
4736+
47194737
"The result of the :delete_device_group mutation"
47204738
type DeleteDeviceGroupResult {
47214739
"The record that was successfully deleted"
@@ -6411,10 +6429,22 @@ type RootMutationType {
64116429
}
64126430

64136431
type RootSubscriptionType {
6432+
deviceGroup(
6433+
"A filter to limit the results"
6434+
filter: DeviceGroupFilterInput
6435+
): device_group_result
64146436
deviceChanged(
64156437
"A filter to limit the results"
64166438
filter: DeviceFilterInput
64176439
): device_changed_result
6440+
baseImage(
6441+
"A filter to limit the results"
6442+
filter: BaseImageFilterInput
6443+
): base_image_result
6444+
baseImageCollection(
6445+
"A filter to limit the results"
6446+
filter: BaseImageCollectionFilterInput
6447+
): base_image_collection_result
64186448
}
64196449

64206450
"""

frontend/src/components/DeviceGroupsTable.tsx

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@
2020

2121
import { useCallback, useEffect, useMemo, useState } from "react";
2222
import { FormattedMessage } from "react-intl";
23-
import { graphql, usePaginationFragment } from "react-relay/hooks";
23+
import {
24+
graphql,
25+
usePaginationFragment,
26+
useSubscription,
27+
} from "react-relay/hooks";
2428
import _ from "lodash";
2529

30+
import { ConnectionHandler } from "relay-runtime";
31+
2632
import type { DeviceGroupsTable_PaginationQuery } from "@/api/__generated__/DeviceGroupsTable_PaginationQuery.graphql";
2733
import type {
2834
DeviceGroupsTable_DeviceGroupFragment$data,
@@ -54,6 +60,40 @@ const DEVICE_GROUPS_TABLE_FRAGMENT = graphql`
5460
}
5561
`;
5662

63+
const DEVICE_GROUP_CREATED_SUBSCRIPTION = graphql`
64+
subscription DeviceGroupsTable_deviceGroupEvent_created_Subscription {
65+
deviceGroup {
66+
created {
67+
id
68+
name
69+
handle
70+
selector
71+
}
72+
}
73+
}
74+
`;
75+
76+
const DEVICE_GROUP_UPDATED_SUBSCRIPTION = graphql`
77+
subscription DeviceGroupsTable_deviceGroupEvent_updated_Subscription {
78+
deviceGroup {
79+
updated {
80+
id
81+
name
82+
handle
83+
selector
84+
}
85+
}
86+
}
87+
`;
88+
89+
const DEVICE_GROUP_DESTROYED_SUBSCRIPTION = graphql`
90+
subscription DeviceGroupsTable_deviceGroupEvent_destroyed_Subscription {
91+
deviceGroup {
92+
destroyed
93+
}
94+
}
95+
`;
96+
5797
type TableRecord = NonNullable<
5898
NonNullable<
5999
DeviceGroupsTable_DeviceGroupFragment$data["deviceGroups"]
@@ -122,6 +162,114 @@ const DeviceGroupsTable = ({
122162
>(DEVICE_GROUPS_TABLE_FRAGMENT, deviceGroupsRef);
123163
const [searchText, setSearchText] = useState<string | null>(null);
124164

165+
const normalizedSearchText = useMemo(
166+
() => (searchText ?? "").trim(),
167+
[searchText],
168+
);
169+
170+
const connectionFilter = useMemo(() => {
171+
if (normalizedSearchText === "") return undefined;
172+
173+
return {
174+
or: [
175+
{ name: { ilike: `%${normalizedSearchText}%` } },
176+
{ handle: { ilike: `%${normalizedSearchText}%` } },
177+
{ selector: { ilike: `%${normalizedSearchText}%` } },
178+
],
179+
};
180+
}, [normalizedSearchText]);
181+
182+
useSubscription(
183+
useMemo(
184+
() => ({
185+
subscription: DEVICE_GROUP_CREATED_SUBSCRIPTION,
186+
variables: {},
187+
updater: (store) => {
188+
const groupEvent = store.getRootField("deviceGroup");
189+
const newGroup = groupEvent?.getLinkedRecord("created");
190+
if (!newGroup) return;
191+
192+
if (normalizedSearchText !== "") {
193+
const search = normalizedSearchText.toLowerCase();
194+
const name = String(newGroup.getValue("name") ?? "").toLowerCase();
195+
const handle = String(
196+
newGroup.getValue("handle") ?? "",
197+
).toLowerCase();
198+
const selector = String(
199+
newGroup.getValue("selector") ?? "",
200+
).toLowerCase();
201+
202+
if (
203+
!name.includes(search) &&
204+
!handle.includes(search) &&
205+
!selector.includes(search)
206+
) {
207+
return;
208+
}
209+
}
210+
211+
const connection = ConnectionHandler.getConnection(
212+
store.getRoot(),
213+
"DeviceGroupsTable_deviceGroups",
214+
connectionFilter ? { filter: connectionFilter } : undefined,
215+
);
216+
if (!connection) return;
217+
218+
const newGroupId = newGroup.getDataID();
219+
const edges = connection.getLinkedRecords("edges") ?? [];
220+
const alreadyPresent = edges.some(
221+
(edge) => edge.getLinkedRecord("node")?.getDataID() === newGroupId,
222+
);
223+
if (alreadyPresent) return;
224+
225+
const edge = ConnectionHandler.createEdge(
226+
store,
227+
connection,
228+
newGroup,
229+
"DeviceGroupEdge",
230+
);
231+
232+
ConnectionHandler.insertEdgeBefore(connection, edge);
233+
},
234+
}),
235+
[connectionFilter, normalizedSearchText],
236+
),
237+
);
238+
239+
useSubscription(
240+
useMemo(
241+
() => ({
242+
subscription: DEVICE_GROUP_UPDATED_SUBSCRIPTION,
243+
variables: {},
244+
}),
245+
[],
246+
),
247+
);
248+
249+
useSubscription(
250+
useMemo(
251+
() => ({
252+
subscription: DEVICE_GROUP_DESTROYED_SUBSCRIPTION,
253+
variables: {},
254+
updater: (store) => {
255+
const groupEvent = store.getRootField("deviceGroup");
256+
const destroyedId = groupEvent?.getValue("destroyed");
257+
if (!destroyedId || typeof destroyedId !== "string") return;
258+
259+
const connection = ConnectionHandler.getConnection(
260+
store.getRoot(),
261+
"DeviceGroupsTable_deviceGroups",
262+
connectionFilter ? { filter: connectionFilter } : undefined,
263+
);
264+
if (!connection) return;
265+
266+
ConnectionHandler.deleteNode(connection, destroyedId);
267+
},
268+
}),
269+
[connectionFilter],
270+
),
271+
);
272+
125273
const debounceRefetch = useMemo(
126274
() =>
127275
_.debounce((text: string) => {
@@ -153,9 +301,9 @@ const DeviceGroupsTable = ({
153301

154302
useEffect(() => {
155303
if (searchText !== null) {
156-
debounceRefetch(searchText);
304+
debounceRefetch(normalizedSearchText);
157305
}
158-
}, [debounceRefetch, searchText]);
306+
}, [debounceRefetch, normalizedSearchText, searchText]);
159307

160308
const loadNextDeviceGroups = useCallback(() => {
161309
if (hasNext && !isLoadingNext) {

0 commit comments

Comments
 (0)