Skip to content

Commit 2900012

Browse files
committed
refactor(frontend): Restructure Release
- Refactor ContainersTable and ReleaseDevicesTable into a stateless, presentational component - Move Relay pagination, search, and filtering logic to Release page Signed-off-by: ArnelaL <arnela.lisic@secomind.com>
1 parent 37e9217 commit 2900012

5 files changed

Lines changed: 334 additions & 280 deletions

File tree

frontend/src/components/ContainersTable.tsx

Lines changed: 111 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,119 @@
1-
/*
2-
* This file is part of Edgehog.
3-
*
4-
* Copyright 2024 - 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-
*/
20-
21-
import { useCallback, useMemo, useState } from "react";
22-
import { FormattedMessage } from "react-intl";
23-
import { graphql, usePaginationFragment } from "react-relay/hooks";
1+
// This file is part of Edgehog.
2+
//
3+
// Copyright 2024-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 _ from "lodash";
20+
import { useMemo, useState } from "react";
2421
import { Stack } from "react-bootstrap";
22+
import { FormattedMessage } from "react-intl";
23+
import { graphql, useFragment } from "react-relay/hooks";
2524

26-
import type { ContainersTable_PaginationQuery } from "@/api/__generated__/ContainersTable_PaginationQuery.graphql";
2725
import type {
28-
ContainersTable_ContainerFragment$data,
29-
ContainersTable_ContainerFragment$key,
30-
} from "@/api/__generated__/ContainersTable_ContainerFragment.graphql";
26+
ContainersTable_ContainerEdgeFragment$data,
27+
ContainersTable_ContainerEdgeFragment$key,
28+
} from "@/api/__generated__/ContainersTable_ContainerEdgeFragment.graphql";
3129

32-
import Form from "@/components/Form";
33-
import StringArrayFormInput from "@/components/StringArrayFormInput";
34-
import MonacoJsonEditor from "@/components/MonacoJsonEditor";
35-
import MultiSelect from "./MultiSelect";
36-
import InfiniteScroll from "./InfiniteScroll";
37-
import DeviceMappingsFormInput from "@/components/DeviceMappingsFormInput";
38-
import { FormRow as BaseFormRow, FormRowProps } from "@/components/FormRow";
39-
import { restartPolicyOptions } from "@/forms/CreateRelease";
40-
import { RECORDS_TO_LOAD_NEXT } from "@/constants";
4130
import CollapseItem, {
4231
useCollapsibleSections,
4332
} from "@/components/CollapseItem";
33+
import DeviceMappingsFormInput from "@/components/DeviceMappingsFormInput";
34+
import Form from "@/components/Form";
35+
import { FormRow as BaseFormRow, FormRowProps } from "@/components/FormRow";
36+
import MonacoJsonEditor from "@/components/MonacoJsonEditor";
37+
import StringArrayFormInput from "@/components/StringArrayFormInput";
38+
import { restartPolicyOptions } from "@/forms/CreateRelease";
39+
import InfiniteScroll from "./InfiniteScroll";
40+
import MultiSelect from "./MultiSelect";
4441

4542
const FormRow = (props: FormRowProps) => (
4643
<BaseFormRow {...props} className="mb-2" />
4744
);
4845

4946
/* eslint-disable relay/unused-fields */
5047
const CONTAINERS_TABLE_FRAGMENT = graphql`
51-
fragment ContainersTable_ContainerFragment on Release
52-
@refetchable(queryName: "ContainersTable_PaginationQuery") {
53-
containers(first: $first, after: $after)
54-
@connection(key: "ContainersTable_containers") {
55-
edges {
56-
node {
57-
id
58-
env {
59-
key
60-
value
48+
fragment ContainersTable_ContainerEdgeFragment on ContainerConnection {
49+
edges {
50+
node {
51+
id
52+
env {
53+
key
54+
value
55+
}
56+
extraHosts
57+
hostname
58+
networkMode
59+
portBindings
60+
binds
61+
restartPolicy
62+
privileged
63+
memory
64+
memorySwap
65+
memoryReservation
66+
memorySwappiness
67+
cpuPeriod
68+
cpuQuota
69+
cpuRealtimePeriod
70+
cpuRealtimeRuntime
71+
tmpfs
72+
storageOpt
73+
readOnlyRootfs
74+
capAdd
75+
capDrop
76+
volumeDriver
77+
image {
78+
reference
79+
credentials {
80+
id
81+
label
82+
username
6183
}
62-
extraHosts
63-
hostname
64-
networkMode
65-
portBindings
66-
binds
67-
restartPolicy
68-
privileged
69-
memory
70-
memorySwap
71-
memoryReservation
72-
memorySwappiness
73-
cpuPeriod
74-
cpuQuota
75-
cpuRealtimePeriod
76-
cpuRealtimeRuntime
77-
tmpfs
78-
storageOpt
79-
readOnlyRootfs
80-
capAdd
81-
capDrop
82-
volumeDriver
83-
image {
84-
reference
85-
credentials {
84+
}
85+
networks {
86+
edges {
87+
node {
8688
id
89+
driver
90+
internal
8791
label
88-
username
92+
options
93+
enableIpv6
8994
}
9095
}
91-
networks {
92-
edges {
93-
node {
96+
}
97+
containerVolumes {
98+
edges {
99+
node {
100+
target
101+
volume {
94102
id
95-
driver
96-
internal
97103
label
104+
driver
98105
options
99-
enableIpv6
100-
}
101-
}
102-
}
103-
containerVolumes {
104-
edges {
105-
node {
106-
target
107-
volume {
108-
id
109-
label
110-
driver
111-
options
112-
}
113106
}
114107
}
115108
}
116-
deviceMappings {
117-
edges {
118-
node {
119-
id
120-
pathInContainer
121-
pathOnHost
122-
cgroupPermissions
123-
}
109+
}
110+
deviceMappings {
111+
edges {
112+
node {
113+
id
114+
pathInContainer
115+
pathOnHost
116+
cgroupPermissions
124117
}
125118
}
126119
}
@@ -163,7 +156,7 @@ const formatJson = (jsonString: unknown) => {
163156

164157
type volumeDetailsProps = {
165158
containerVolumes: NonNullable<
166-
ContainersTable_ContainerFragment$data["containers"]["edges"]
159+
ContainersTable_ContainerEdgeFragment$data["edges"]
167160
>[number]["node"]["containerVolumes"];
168161
containerIndex: number;
169162
};
@@ -265,14 +258,14 @@ const VolumeDetails = ({
265258
);
266259
};
267260

268-
type networkDetailsProps = {
261+
type NetworkDetailsProps = {
269262
networks: NonNullable<
270-
ContainersTable_ContainerFragment$data["containers"]["edges"]
263+
ContainersTable_ContainerEdgeFragment$data["edges"]
271264
>[number]["node"]["networks"];
272265
containerIndex: number;
273266
};
274267

275-
const NetworkDetails = ({ networks, containerIndex }: networkDetailsProps) => {
268+
const NetworkDetails = ({ networks, containerIndex }: NetworkDetailsProps) => {
276269
const { toggleSection: toggleNetwork, isSectionOpen } =
277270
useCollapsibleSections<number>(
278271
networks.edges?.map((_, index) => index) ?? [],
@@ -389,7 +382,7 @@ const NetworkDetails = ({ networks, containerIndex }: networkDetailsProps) => {
389382

390383
type DeviceMappingDetailsProps = {
391384
deviceMappings: NonNullable<
392-
ContainersTable_ContainerFragment$data["containers"]["edges"]
385+
ContainersTable_ContainerEdgeFragment$data["edges"]
393386
>[number]["node"]["deviceMappings"];
394387
containerIndex?: number;
395388
};
@@ -433,7 +426,7 @@ const DeviceMappingDetails = ({
433426
};
434427

435428
type ContainerRecord = NonNullable<
436-
ContainersTable_ContainerFragment$data["containers"]["edges"]
429+
ContainersTable_ContainerEdgeFragment$data["edges"]
437430
>[number]["node"];
438431
type ContainerEnv = ContainerRecord["env"];
439432

@@ -929,25 +922,24 @@ const ContainerDetails = ({ container, index }: ContainerDetailsProps) => {
929922

930923
type ContainersTableProps = {
931924
className?: string;
932-
containersRef: ContainersTable_ContainerFragment$key;
925+
containersRef: ContainersTable_ContainerEdgeFragment$key;
926+
loading?: boolean;
927+
onLoadMore?: () => void;
933928
};
934929

935930
const ContainersTable = ({
936931
className,
937932
containersRef,
933+
loading = false,
934+
onLoadMore,
938935
}: ContainersTableProps) => {
939-
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
940-
ContainersTable_PaginationQuery,
941-
ContainersTable_ContainerFragment$key
942-
>(CONTAINERS_TABLE_FRAGMENT, containersRef);
943-
944-
const loadNextContainers = useCallback(() => {
945-
if (hasNext && !isLoadingNext) loadNext(RECORDS_TO_LOAD_NEXT);
946-
}, [hasNext, isLoadingNext, loadNext]);
947-
948-
const containers: ContainerRecord[] = useMemo(() => {
949-
return data.containers?.edges?.map((edge) => edge?.node) ?? [];
950-
}, [data]);
936+
const containersFragment = useFragment(
937+
CONTAINERS_TABLE_FRAGMENT,
938+
containersRef || null,
939+
);
940+
const containers = useMemo<ContainerRecord[]>(() => {
941+
return _.compact(containersFragment?.edges?.map((e) => e?.node)) ?? [];
942+
}, [containersFragment]);
951943

952944
const { toggleSection: toggleIndex, isSectionOpen } =
953945
useCollapsibleSections<number>(containers.map((_, index) => index));
@@ -971,8 +963,8 @@ const ContainersTable = ({
971963
<InfiniteScroll
972964
key={container.id}
973965
className={className}
974-
loading={isLoadingNext}
975-
onLoadMore={hasNext ? loadNextContainers : undefined}
966+
loading={loading}
967+
onLoadMore={onLoadMore}
976968
>
977969
<CollapseItem
978970
type="card-parent"

frontend/src/components/DeviceMappingsFormInput.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ import {
2626
} from "react-hook-form";
2727
import { FormattedMessage } from "react-intl";
2828

29-
import { ContainersTable_ContainerFragment$data } from "@/api/__generated__/ContainersTable_ContainerFragment.graphql";
29+
import { ContainersTable_ContainerEdgeFragment$data } from "@/api/__generated__/ContainersTable_ContainerEdgeFragment.graphql";
3030

3131
import Button from "@/components/Button";
3232
import Form from "@/components/Form";
3333
import Icon from "@/components/Icon";
3434
import { ReleaseFormData } from "@/forms/validation";
3535

3636
type DeviceMappingsData = NonNullable<
37-
ContainersTable_ContainerFragment$data["containers"]["edges"]
37+
ContainersTable_ContainerEdgeFragment$data["edges"]
3838
>[number]["node"]["deviceMappings"];
3939

4040
type ReadOnlyFormInputProps = {

0 commit comments

Comments
 (0)