Skip to content

Commit 2a05ecd

Browse files
committed
feat(frontend): Add repository management interface
Implement full CRUD functionality for managing repositories: - Add `Files Management` section to sidebar with Repositories item - Add Repositories list page with search and pagination - Add repository creation page with form validation - Add repository edit page with update and delete actions Signed-off-by: Omar <omar.brbutovic@secomind.com>
1 parent dea4541 commit 2a05ecd

File tree

13 files changed

+1322
-3
lines changed

13 files changed

+1322
-3
lines changed

frontend/src/App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ import DeploymentCampaignsPage from "@/pages/DeploymentCampaigns";
6969
import DeploymentCampaign from "@/pages/DeploymentCampaign";
7070
import DeploymentCampaignCreate from "@/pages/DeploymentCampaignCreate";
7171
import Deployment from "@/pages/Deployment";
72+
import Repositories from "@/pages/Repositories";
73+
import RepositoryCreatePage from "@/pages/RepositoryCreate";
74+
import Repository from "@/pages/Repository";
7275

7376
import { hideNavigationElements } from "@/api";
7477
import { bugs, repository, version } from "../package.json";
@@ -129,6 +132,9 @@ const authenticatedRoutes: RouterRule[] = [
129132
{ path: Route.deploymentCampaigns, element: <DeploymentCampaignsPage /> },
130133
{ path: Route.deploymentCampaignsNew, element: <DeploymentCampaignCreate /> },
131134
{ path: Route.deploymentCampaignsEdit, element: <DeploymentCampaign /> },
135+
{ path: Route.repositories, element: <Repositories /> },
136+
{ path: Route.repositoryNew, element: <RepositoryCreatePage /> },
137+
{ path: Route.repositoryEdit, element: <Repository /> },
132138
{ path: Route.logout, element: <Logout /> },
133139
{ path: "*", element: <Navigate to={Route.devices} replace /> },
134140
];

frontend/src/Navigation.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This file is part of Edgehog.
33
4-
Copyright 2021-2025 SECO Mind Srl
4+
Copyright 2021-2026 SECO Mind Srl
55
66
Licensed under the Apache License, Version 2.0 (the "License");
77
you may not use this file except in compliance with the License.
@@ -72,6 +72,9 @@ enum Route {
7272
deploymentCampaigns = "/deployment-campaigns",
7373
deploymentCampaignsNew = "/deployment-campaigns/new",
7474
deploymentCampaignsEdit = "/deployment-campaigns/:deploymentCampaignId",
75+
repositories = "/repositories",
76+
repositoryNew = "/repositories/new",
77+
repositoryEdit = "/repositories/:repositoryId/edit",
7578
login = "/login",
7679
logout = "/logout",
7780
}
@@ -129,6 +132,8 @@ const matchingParametricRoute = (
129132
case Route.deployments:
130133
case Route.deploymentCampaigns:
131134
case Route.deploymentCampaignsNew:
135+
case Route.repositories:
136+
case Route.repositoryNew:
132137
case Route.login:
133138
case Route.logout:
134139
return { route } as ParametricRoute;
@@ -281,6 +286,14 @@ const matchingParametricRoute = (
281286
params: { deploymentCampaignId: params.deploymentCampaignId },
282287
}
283288
: null;
289+
290+
case Route.repositoryEdit:
291+
return params && typeof params["repositoryId"] === "string"
292+
? {
293+
route,
294+
params: { repositoryId: params.repositoryId },
295+
}
296+
: null;
284297
}
285298
};
286299

@@ -495,6 +508,18 @@ const routeTitles: Record<Route, MessageDescriptor> = defineMessages({
495508
id: "navigation.routeTitle.DeploymentCampaignsEdit",
496509
defaultMessage: "Edit Campaign",
497510
},
511+
[Route.repositories]: {
512+
id: "navigation.routeTitle.Repositories",
513+
defaultMessage: "Repositories",
514+
},
515+
[Route.repositoryNew]: {
516+
id: "navigation.routeTitle.RepositoryNew",
517+
defaultMessage: "Create Repository",
518+
},
519+
[Route.repositoryEdit]: {
520+
id: "navigation.routeTitle.RepositoryEdit",
521+
defaultMessage: "Edit Repository",
522+
},
498523
});
499524

500525
export {

frontend/src/components/Icon.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
faSpinner,
4141
faStop,
4242
faSwatchbook,
43+
faFolderOpen,
4344
faTabletAlt,
4445
faTimes,
4546
faTrash,
@@ -65,6 +66,7 @@ const icons = {
6566
delete: faTrash,
6667
devices: faTabletAlt,
6768
deviceGroups: faDatabase,
69+
folder: faFolderOpen,
6870
github: faGithub,
6971
models: faSwatchbook,
7072
os: faCompactDisc,

frontend/src/components/Page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* This file is part of Edgehog.
33
*
4-
* Copyright 2021-2025 SECO Mind Srl
4+
* Copyright 2021-2026 SECO Mind Srl
55
*
66
* Licensed under the Apache License, Version 2.0 (the "License");
77
* you may not use this file except in compliance with the License.
@@ -95,6 +95,7 @@ const useBreadcrumbItems = (): BreadcrumbItem[] => {
9595
case Route.networks:
9696
case Route.deployments:
9797
case Route.deploymentCampaigns:
98+
case Route.repositories:
9899
case Route.login:
99100
case Route.logout:
100101
return [currentRoute];
@@ -181,6 +182,11 @@ const useBreadcrumbItems = (): BreadcrumbItem[] => {
181182
case Route.deploymentCampaignsEdit:
182183
case Route.deploymentCampaignsNew:
183184
return [{ route: Route.deploymentCampaigns }, currentRoute];
185+
186+
case Route.repositoryNew:
187+
case Route.repositoryEdit:
188+
return [{ route: Route.repositories }, currentRoute];
189+
184190
default:
185191
return [];
186192
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 _ from "lodash";
20+
import { useMemo } from "react";
21+
import { FormattedMessage } from "react-intl";
22+
import { graphql, useFragment } from "react-relay/hooks";
23+
24+
import type {
25+
RepositoriesTable_RepositoryEdgeFragment$data,
26+
RepositoriesTable_RepositoryEdgeFragment$key,
27+
} from "@/api/__generated__/RepositoriesTable_RepositoryEdgeFragment.graphql";
28+
29+
import InfiniteTable from "@/components/InfiniteTable";
30+
import { createColumnHelper } from "@/components/Table";
31+
import { Link, Route } from "@/Navigation";
32+
33+
// We use graphql fields below in columns configuration
34+
/* eslint-disable relay/unused-fields */
35+
const REPOSITORIES_FRAGMENT = graphql`
36+
fragment RepositoriesTable_RepositoryEdgeFragment on RepositoryConnection {
37+
edges {
38+
node {
39+
id
40+
name
41+
handle
42+
}
43+
}
44+
}
45+
`;
46+
47+
type TableRecord = NonNullable<
48+
NonNullable<RepositoriesTable_RepositoryEdgeFragment$data>["edges"]
49+
>[number]["node"];
50+
51+
const columnHelper = createColumnHelper<TableRecord>();
52+
const columns = [
53+
columnHelper.accessor("name", {
54+
header: () => (
55+
<FormattedMessage
56+
id="components.RepositoriesTable.nameTitle"
57+
defaultMessage="Repository Name"
58+
description="Title for the Name column of the repositories table"
59+
/>
60+
),
61+
cell: ({ row, getValue }) => (
62+
<Link
63+
route={Route.repositoryEdit}
64+
params={{ repositoryId: row.original.id }}
65+
>
66+
{getValue()}
67+
</Link>
68+
),
69+
}),
70+
columnHelper.accessor("handle", {
71+
header: () => (
72+
<FormattedMessage
73+
id="components.RepositoriesTable.handleTitle"
74+
defaultMessage="Handle"
75+
description="Title for the Handle column of the repositories table"
76+
/>
77+
),
78+
}),
79+
];
80+
81+
type Props = {
82+
className?: string;
83+
repositoriesRef: RepositoriesTable_RepositoryEdgeFragment$key;
84+
loading?: boolean;
85+
onLoadMore?: () => void;
86+
};
87+
88+
const RepositoriesTable = ({
89+
className,
90+
repositoriesRef,
91+
loading = false,
92+
onLoadMore,
93+
}: Props) => {
94+
const repositoriesFragment = useFragment(
95+
REPOSITORIES_FRAGMENT,
96+
repositoriesRef || null,
97+
);
98+
99+
const repositories = useMemo<TableRecord[]>(() => {
100+
return _.compact(repositoriesFragment?.edges?.map((e) => e?.node)) ?? [];
101+
}, [repositoriesFragment]);
102+
103+
return (
104+
<InfiniteTable
105+
className={className}
106+
columns={columns}
107+
data={repositories}
108+
loading={loading}
109+
onLoadMore={onLoadMore}
110+
hideSearch
111+
/>
112+
);
113+
};
114+
115+
export default RepositoriesTable;

frontend/src/components/Sidebar.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* This file is part of Edgehog.
33
*
4-
* Copyright 2021-2025 SECO Mind Srl
4+
* Copyright 2021-2026 SECO Mind Srl
55
*
66
* Licensed under the Apache License, Version 2.0 (the "License");
77
* you may not use this file except in compliance with the License.
@@ -128,6 +128,31 @@ const Sidebar = () => (
128128
route={Route.channels}
129129
activeRoutes={[Route.channels, Route.channelsEdit, Route.channelsNew]}
130130
/>
131+
<SidebarItemGroup
132+
label={
133+
<FormattedMessage
134+
id="components.Sidebar.filesManagementGroupLabel"
135+
defaultMessage="Files Management"
136+
/>
137+
}
138+
icon="folder"
139+
>
140+
<SidebarItem
141+
label={
142+
<FormattedMessage
143+
id="components.Sidebar.repositoriesLabel"
144+
defaultMessage="Repositories"
145+
/>
146+
}
147+
route={Route.repositories}
148+
activeRoutes={[
149+
Route.repositories,
150+
Route.repositoryNew,
151+
Route.repositoryEdit,
152+
]}
153+
/>
154+
</SidebarItemGroup>
155+
131156
<SidebarItemGroup
132157
label={
133158
<FormattedMessage

0 commit comments

Comments
 (0)