Skip to content

Commit 668ebd3

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 3e09933 commit 668ebd3

2 files changed

Lines changed: 178 additions & 3 deletions

File tree

frontend/src/api/schema.graphql

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4716,6 +4716,12 @@ type ForwarderConfig {
47164716
secureSessions: Boolean!
47174717
}
47184718

4719+
type device_group_event_result {
4720+
created: DeviceGroup
4721+
updated: DeviceGroup
4722+
destroyed: ID
4723+
}
4724+
47194725
"The result of the :delete_device_group mutation"
47204726
type DeleteDeviceGroupResult {
47214727
"The record that was successfully deleted"
@@ -5005,6 +5011,15 @@ type Tag {
50055011
name: String!
50065012
}
50075013

5014+
"The result of the :cancel_ota_operation mutation"
5015+
type CancelOtaOperationResult {
5016+
"The successful result of the mutation"
5017+
result: OtaOperation
5018+
5019+
"Any errors generated, if the mutation failed"
5020+
errors: [MutationError!]!
5021+
}
5022+
50085023
"The result of the :create_manual_ota_operation mutation"
50095024
type CreateManualOtaOperationResult {
50105025
"The successful result of the mutation"
@@ -6261,6 +6276,9 @@ type RootMutationType {
62616276
input: CreateManualOtaOperationInput!
62626277
): CreateManualOtaOperationResult
62636278

6279+
"Cancels an OTA update operation"
6280+
cancelOtaOperation(id: ID!): CancelOtaOperationResult
6281+
62646282
"Creates a new device group."
62656283
createDeviceGroup(input: CreateDeviceGroupInput!): CreateDeviceGroupResult
62666284

@@ -6399,6 +6417,10 @@ type RootMutationType {
63996417
}
64006418

64016419
type RootSubscriptionType {
6420+
deviceGroupEvent(
6421+
"A filter to limit the results"
6422+
filter: DeviceGroupFilterInput
6423+
): device_group_event_result
64026424
deviceChanged(
64036425
"A filter to limit the results"
64046426
filter: DeviceFilterInput

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+
deviceGroupEvent {
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+
deviceGroupEvent {
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+
deviceGroupEvent {
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("deviceGroupEvent");
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("deviceGroupEvent");
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)