Skip to content

Commit 8dfbab1

Browse files
committed
refactor(frontend): Restructure Application
- Refactor ReleasesTable and ApplicationDevicesTable into a stateless, presentational component - Move Relay pagination, search, and filtering logic to Application page - Centralize delete behavior and modal handling Signed-off-by: ArnelaL <arnela.lisic@secomind.com>
1 parent cc11b69 commit 8dfbab1

5 files changed

Lines changed: 396 additions & 333 deletions

File tree

frontend/src/components/ApplicationDevicesTable.tsx

Lines changed: 62 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,58 @@
1-
/*
2-
* This file is part of Edgehog.
3-
*
4-
* Copyright 2025 SECO Mind Srl
5-
*
6-
* Licensed under the Apache License, Version 2.0 (the "License");
7-
* you may not use this file except in compliance with the License.
8-
* You may obtain a copy of the License at
9-
*
10-
* http://www.apache.org/licenses/LICENSE-2.0
11-
*
12-
* Unless required by applicable law or agreed to in writing, software
13-
* distributed under the License is distributed on an "AS IS" BASIS,
14-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15-
* See the License for the specific language governing permissions and
16-
* limitations under the License.
17-
*
18-
* SPDX-License-Identifier: Apache-2.0
19-
*/
1+
// This file is part of Edgehog.
2+
//
3+
// Copyright 2025, 2026 SECO Mind Srl
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
//
17+
// SPDX-License-Identifier: Apache-2.0
2018

19+
import _ from "lodash";
20+
import { useMemo } from "react";
2121
import { FormattedMessage } from "react-intl";
22-
import { graphql, usePaginationFragment } from "react-relay/hooks";
22+
import { graphql, useFragment } from "react-relay/hooks";
2323

24-
import type { ApplicationDevicesTable_PaginationQuery } from "@/api/__generated__/ApplicationDevicesTable_PaginationQuery.graphql";
2524
import type {
26-
ApplicationDevicesTable_ReleaseFragment$data,
27-
ApplicationDevicesTable_ReleaseFragment$key,
28-
} from "@/api/__generated__/ApplicationDevicesTable_ReleaseFragment.graphql";
25+
ApplicationDevicesTable_ReleaseEdgeFragment$data,
26+
ApplicationDevicesTable_ReleaseEdgeFragment$key,
27+
} from "@/api/__generated__/ApplicationDevicesTable_ReleaseEdgeFragment.graphql";
2928

3029
import ConnectionStatus from "@/components/ConnectionStatus";
3130
import DeploymentStateComponent, {
3231
type DeploymentState,
3332
} from "@/components/DeploymentState";
34-
import Table, { createColumnHelper } from "@/components/Table";
33+
import { createColumnHelper } from "@/components/Table";
3534
import { Link, Route } from "@/Navigation";
35+
import InfiniteTable from "./InfiniteTable";
3636

3737
// We use graphql fields below in columns configuration
3838
/* eslint-disable relay/unused-fields */
3939
const APPLICATION_DEVICES_TABLE_FRAGMENT = graphql`
40-
fragment ApplicationDevicesTable_ReleaseFragment on Application
41-
@refetchable(queryName: "ApplicationDevicesTable_PaginationQuery")
42-
@argumentDefinitions(filter: { type: "ReleaseFilterInput" }) {
43-
releases(first: $first, after: $after, filter: $filter)
44-
@connection(key: "ApplicationDevicesTable_releases") {
45-
edges {
46-
node {
47-
deployments {
48-
edges {
49-
node {
40+
fragment ApplicationDevicesTable_ReleaseEdgeFragment on ReleaseConnection {
41+
edges {
42+
node {
43+
deployments {
44+
edges {
45+
node {
46+
id
47+
state
48+
isReady
49+
device {
5050
id
51-
state
52-
isReady
53-
device {
54-
id
55-
name
56-
online
57-
}
58-
release {
59-
version
60-
}
51+
name
52+
online
53+
}
54+
release {
55+
version
6156
}
6257
}
6358
}
@@ -68,7 +63,7 @@ const APPLICATION_DEVICES_TABLE_FRAGMENT = graphql`
6863
`;
6964

7065
type ReleaseRecord = NonNullable<
71-
ApplicationDevicesTable_ReleaseFragment$data["releases"]["edges"]
66+
ApplicationDevicesTable_ReleaseEdgeFragment$data["edges"]
7267
>[number]["node"];
7368

7469
type TableRecord = NonNullable<
@@ -77,28 +72,30 @@ type TableRecord = NonNullable<
7772

7873
type ApplicationDevicesTableProps = {
7974
className?: string;
80-
applicationDevicesRef: ApplicationDevicesTable_ReleaseFragment$key;
81-
hideSearch?: boolean;
75+
applicationDevicesRef: ApplicationDevicesTable_ReleaseEdgeFragment$key;
76+
loading?: boolean;
77+
onLoadMore?: () => void;
8278
};
8379

8480
const ApplicationDevicesTable = ({
8581
className,
8682
applicationDevicesRef,
87-
hideSearch = false,
83+
loading = false,
84+
onLoadMore,
8885
}: ApplicationDevicesTableProps) => {
89-
const { data } = usePaginationFragment<
90-
ApplicationDevicesTable_PaginationQuery,
91-
ApplicationDevicesTable_ReleaseFragment$key
92-
>(APPLICATION_DEVICES_TABLE_FRAGMENT, applicationDevicesRef);
86+
const applicationDevicesFragment = useFragment(
87+
APPLICATION_DEVICES_TABLE_FRAGMENT,
88+
applicationDevicesRef || null,
89+
);
90+
const releases = useMemo<ReleaseRecord[]>(() => {
91+
return (
92+
_.compact(applicationDevicesFragment?.edges?.map((e) => e?.node)) ?? []
93+
);
94+
}, [applicationDevicesFragment]);
9395

94-
const releasesData =
95-
data.releases.edges?.map((edge) => ({
96-
deployments:
97-
edge.node.deployments.edges?.map(
98-
(deploymentEdge) => deploymentEdge.node,
99-
) ?? [],
100-
})) ?? [];
101-
const tableData = releasesData.flatMap((release) => release.deployments);
96+
const tableData = releases.flatMap(
97+
(release) => release.deployments?.edges?.map((edge) => edge.node) ?? [],
98+
);
10299

103100
const columnHelper = createColumnHelper<TableRecord>();
104101
const columns = [
@@ -158,11 +155,13 @@ const ApplicationDevicesTable = ({
158155

159156
return (
160157
<div>
161-
<Table
158+
<InfiniteTable
162159
className={className}
163160
columns={columns}
164161
data={tableData}
165-
hideSearch={hideSearch}
162+
loading={loading}
163+
onLoadMore={onLoadMore}
164+
hideSearch
166165
/>
167166
</div>
168167
);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// This file is part of Edgehog.
2+
//
3+
// Copyright 2026 SECO Mind Srl
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
//
17+
// SPDX-License-Identifier: Apache-2.0
18+
19+
import { useCallback } from "react";
20+
import { FormattedMessage } from "react-intl";
21+
import { graphql, useMutation } from "react-relay";
22+
23+
import type { DeleteReleaseModal_deleteRelease_Mutation } from "@/api/__generated__/DeleteReleaseModal_deleteRelease_Mutation.graphql";
24+
25+
import DeleteModal from "./DeleteModal";
26+
27+
const DELETE_RELEASE_MUTATION = graphql`
28+
mutation DeleteReleaseModal_deleteRelease_Mutation($id: ID!) {
29+
deleteRelease(id: $id) {
30+
result {
31+
id
32+
}
33+
}
34+
}
35+
`;
36+
37+
type Release = {
38+
id: string;
39+
version: string;
40+
};
41+
42+
type DeleteReleaseModalProps<R extends Release> = {
43+
releaseToDelete: R;
44+
onCancel: () => void;
45+
onConfirm: () => void;
46+
setErrorFeedback: (errorMessages: React.ReactNode) => void;
47+
};
48+
const DeleteReleaseModal = <R extends Release>({
49+
releaseToDelete,
50+
onCancel,
51+
onConfirm,
52+
setErrorFeedback,
53+
}: DeleteReleaseModalProps<R>) => {
54+
const [deleteRelease, isDeletingRelease] =
55+
useMutation<DeleteReleaseModal_deleteRelease_Mutation>(
56+
DELETE_RELEASE_MUTATION,
57+
);
58+
59+
const handleDeleteRelease = useCallback(() => {
60+
deleteRelease({
61+
variables: { id: releaseToDelete.id },
62+
onCompleted(data, errors) {
63+
if (errors) {
64+
const errorFeedback = errors
65+
.map((error) => error.message)
66+
.join(". \n");
67+
return setErrorFeedback(errorFeedback);
68+
}
69+
setErrorFeedback(null);
70+
return onConfirm();
71+
},
72+
onError() {
73+
setErrorFeedback(
74+
<FormattedMessage
75+
id="components.DeleteReleaseModal.deletionErrorFeedback"
76+
defaultMessage="Could not delete the release, please try again."
77+
/>,
78+
);
79+
},
80+
updater(store, response) {
81+
const deletedId = response?.deleteRelease?.result?.id;
82+
if (!deletedId) return;
83+
84+
store.delete(deletedId);
85+
},
86+
});
87+
}, [releaseToDelete, onConfirm, deleteRelease, setErrorFeedback]);
88+
89+
return (
90+
<DeleteModal
91+
confirmText={releaseToDelete.version || ""}
92+
onCancel={onCancel}
93+
onConfirm={handleDeleteRelease}
94+
isDeleting={isDeletingRelease}
95+
title={
96+
<FormattedMessage
97+
id="components.DeleteReleaseModal.deleteModal.title"
98+
defaultMessage="Delete Release"
99+
/>
100+
}
101+
>
102+
<p>
103+
<FormattedMessage
104+
id="components.ReleasesTable.deleteModal.description"
105+
defaultMessage="This action cannot be undone. This will permanently delete the release."
106+
/>
107+
</p>
108+
</DeleteModal>
109+
);
110+
};
111+
export default DeleteReleaseModal;

0 commit comments

Comments
 (0)