Skip to content

Commit 6496ac0

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 27799fd commit 6496ac0

2 files changed

Lines changed: 198 additions & 3 deletions

File tree

frontend/src/api/schema.graphql

Lines changed: 42 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"
@@ -5005,6 +5023,15 @@ type Tag {
50055023
name: String!
50065024
}
50075025

5026+
"The result of the :cancel_ota_operation mutation"
5027+
type CancelOtaOperationResult {
5028+
"The successful result of the mutation"
5029+
result: OtaOperation
5030+
5031+
"Any errors generated, if the mutation failed"
5032+
errors: [MutationError!]!
5033+
}
5034+
50085035
"The result of the :create_manual_ota_operation mutation"
50095036
type CreateManualOtaOperationResult {
50105037
"The successful result of the mutation"
@@ -6261,6 +6288,9 @@ type RootMutationType {
62616288
input: CreateManualOtaOperationInput!
62626289
): CreateManualOtaOperationResult
62636290

6291+
"Cancels an OTA update operation"
6292+
cancelOtaOperation(id: ID!): CancelOtaOperationResult
6293+
62646294
"Creates a new device group."
62656295
createDeviceGroup(input: CreateDeviceGroupInput!): CreateDeviceGroupResult
62666296

@@ -6399,10 +6429,22 @@ type RootMutationType {
63996429
}
64006430

64016431
type RootSubscriptionType {
6432+
deviceGroup(
6433+
"A filter to limit the results"
6434+
filter: DeviceGroupFilterInput
6435+
): device_group_result
64026436
deviceChanged(
64036437
"A filter to limit the results"
64046438
filter: DeviceFilterInput
64056439
): 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
64066448
}
64076449

64086450
"""

frontend/src/components/DeviceGroupsTable.tsx

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

154307
useEffect(() => {
155308
if (searchText !== null) {
156-
debounceRefetch(searchText);
309+
debounceRefetch(normalizedSearchText);
157310
}
158-
}, [debounceRefetch, searchText]);
311+
}, [debounceRefetch, normalizedSearchText, searchText]);
159312

160313
const loadNextDeviceGroups = useCallback(() => {
161314
if (hasNext && !isLoadingNext) {

0 commit comments

Comments
 (0)