Skip to content

Commit 4b68546

Browse files
committed
Fairly large refactoring with a new UI/dashboard
1 parent 3666e39 commit 4b68546

17 files changed

+637
-168
lines changed

.github/workflows/build-and-publish.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
branches:
66
- main
77
tags:
8-
- '*'
8+
- "*"
99

1010
jobs:
1111
bake:

dev/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM nginx
2+
COPY default.conf /etc/nginx/conf.d/default.conf

dev/default.conf

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
server {
2+
listen 80;
3+
server_name localhost;
4+
5+
location / {
6+
root /usr/share/nginx/html;
7+
index index.html index.htm;
8+
try_files $uri $uri/ /index.html;
9+
10+
# CORS headers
11+
add_header 'Access-Control-Allow-Origin' '*' always;
12+
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
13+
add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization' always;
14+
15+
# Handle preflight requests
16+
if ($request_method = 'OPTIONS') {
17+
add_header 'Access-Control-Allow-Origin' '*';
18+
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
19+
add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, Authorization';
20+
add_header 'Access-Control-Max-Age' 1728000;
21+
add_header 'Content-Type' 'text/plain; charset=utf-8';
22+
add_header 'Content-Length' 0;
23+
return 204;
24+
}
25+
}
26+
}

dev/src/catalog.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
tags:
2+
- name: demos
3+
label: Demos
4+
5+
labspaces:
6+
- title: DHI Demo
7+
description: |
8+
Provides a demo of Docker' Hardened Images
9+
repo: https://github.com/dockersamples/labspace-agentic-apps-with-docker
10+
publishedRepo: dockersamples/labspace-agentic-apps-with-docker
11+
author: Docker
12+
datePublished: 2025-10-21
13+
level: Beginner
14+
tags:
15+
- demos

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
5+
<link rel="icon" type="image/svg+xml" href="/beaker.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>Labspace Extension</title>
88
<link

public/beaker.svg

Lines changed: 9 additions & 0 deletions
Loading

public/vite.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/App.jsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import "./App.scss";
2+
import { CatalogContextProvider } from "./CatalogContext";
23
import { DockerContextProvider } from "./DockerContext";
34
import { Home } from "./Home";
45

56
function App() {
67
return (
7-
<DockerContextProvider>
8-
<Home />
9-
</DockerContextProvider>
8+
<CatalogContextProvider>
9+
<DockerContextProvider>
10+
<Home />
11+
</DockerContextProvider>
12+
</CatalogContextProvider>
1013
);
1114
}
1215

src/CatalogContext.jsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import {
2+
createContext,
3+
useCallback,
4+
useContext,
5+
useEffect,
6+
useMemo,
7+
useState,
8+
} from "react";
9+
import Spinner from "react-bootstrap/Spinner";
10+
import { parse } from "yaml";
11+
12+
const CatalogContext = createContext();
13+
14+
const CATALOGS = [
15+
{
16+
name: "Awesome Labspaces",
17+
url: "https://raw.githubusercontent.com/dockersamples/awesome-labspaces/refs/heads/main/catalog.yaml",
18+
},
19+
];
20+
21+
const LOCALSTORAGE_CATALOG_KEY = "labspaces.catalogs";
22+
const LOCALSTORAGE_ADDITIONAL_LABSPACES_KEY = "labspaces.additionalLabspaces";
23+
24+
export function CatalogContextProvider({ children }) {
25+
const [catalogs, setCatalogs] = useState(
26+
localStorage.getItem(LOCALSTORAGE_CATALOG_KEY)
27+
? JSON.parse(localStorage.getItem(LOCALSTORAGE_CATALOG_KEY))
28+
: CATALOGS,
29+
);
30+
const [catalogDetails, setCatalogDetails] = useState(null);
31+
const [customLabspaces, setCustomLabspaces] = useState(
32+
localStorage.getItem(LOCALSTORAGE_ADDITIONAL_LABSPACES_KEY)
33+
? JSON.parse(localStorage.getItem(LOCALSTORAGE_ADDITIONAL_LABSPACES_KEY))
34+
: [],
35+
);
36+
37+
useEffect(() => {
38+
localStorage.setItem(LOCALSTORAGE_CATALOG_KEY, JSON.stringify(catalogs));
39+
}, [catalogs]);
40+
41+
useEffect(() => {
42+
localStorage.setItem(
43+
LOCALSTORAGE_ADDITIONAL_LABSPACES_KEY,
44+
JSON.stringify(customLabspaces),
45+
);
46+
}, [customLabspaces]);
47+
48+
const addCatalog = useCallback(
49+
(name, url) => {
50+
setCatalogs((catalog) => [...catalog, { name, url }]);
51+
},
52+
[setCatalogs],
53+
);
54+
55+
const removeCatalog = useCallback(
56+
(url) => {
57+
setCatalogs((catalogs) => catalogs.filter((c) => c.url !== url));
58+
},
59+
[setCatalogs],
60+
);
61+
62+
const tags = useMemo(() => {
63+
if (!catalogDetails) return null;
64+
const allTags = [];
65+
catalogDetails.forEach((catalog) => {
66+
catalog.tags.forEach((tag) => {
67+
if (!allTags.find((t) => t.label === tag.label)) {
68+
allTags.push(tag);
69+
}
70+
});
71+
});
72+
return allTags.sort((a, b) => a.label.localeCompare(b.label));
73+
}, [catalogDetails]);
74+
75+
const labspaces = useMemo(() => {
76+
if (!catalogDetails) return null;
77+
const allLabspaces = [...customLabspaces];
78+
catalogDetails.forEach((catalog) => {
79+
catalog.labspaces.forEach((labspace) => {
80+
allLabspaces.push({ ...labspace, catalog: catalog.url });
81+
});
82+
});
83+
return allLabspaces.sort((a, b) => {
84+
if (a.highlighted && !b.highlighted) return -1;
85+
if (!a.highlighted && b.highlighted) return 1;
86+
return a.title.localeCompare(b.title);
87+
});
88+
}, [catalogDetails, customLabspaces]);
89+
90+
useEffect(() => {
91+
Promise.all(
92+
catalogs.map((catalog) =>
93+
fetch(catalog.url)
94+
.then((res) => res.text())
95+
.then((text) => parse(text))
96+
.then((data) => ({
97+
...catalog,
98+
tags: data.tags || [],
99+
labspaces: data.labspaces || [],
100+
})),
101+
),
102+
).then((results) => setCatalogDetails(results));
103+
}, [catalogs]);
104+
105+
const addCustomLabspace = useCallback(
106+
(title, publishedRepo) => {
107+
const newLabspace = { title, publishedRepo };
108+
setCustomLabspaces((labspaces) => [...labspaces, newLabspace]);
109+
},
110+
[setCustomLabspaces],
111+
);
112+
113+
const removeCustomLabspace = useCallback(
114+
(publishedRepo) => {
115+
setCustomLabspaces((labs) =>
116+
labs.filter((l) => l.publishedRepo !== publishedRepo),
117+
);
118+
},
119+
[setCustomLabspaces],
120+
);
121+
122+
if (!catalogDetails || !labspaces || !tags) {
123+
return (
124+
<div
125+
className="d-flex justify-content-center align-items-center"
126+
style={{ height: "100vh" }}
127+
>
128+
<Spinner animation="border" role="status">
129+
<span className="visually-hidden">Loading...</span>
130+
</Spinner>
131+
</div>
132+
);
133+
}
134+
135+
return (
136+
<CatalogContext.Provider
137+
value={{
138+
catalogs: catalogDetails,
139+
addCatalog,
140+
removeCatalog,
141+
tags,
142+
labspaces,
143+
addCustomLabspace,
144+
removeCustomLabspace,
145+
}}
146+
>
147+
{children}
148+
</CatalogContext.Provider>
149+
);
150+
}
151+
152+
export const useCatalogs = () => useContext(CatalogContext);

src/DockerContext.jsx

Lines changed: 5 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ import Spinner from "react-bootstrap/Spinner";
1010

1111
import { parse } from "yaml";
1212
import { LogProcessor } from "./logProcessor.js";
13+
import { useCatalogs } from "./CatalogContext.jsx";
1314

1415
const CATALOGS = [
15-
"https://raw.githubusercontent.com/dockersamples/awesome-labspaces/refs/heads/main/catalog.yaml",
16+
// "https://raw.githubusercontent.com/dockersamples/awesome-labspaces/refs/heads/main/catalog.yaml",
17+
"http://localhost/catalog.yaml",
1618
];
1719

1820
const DockerContext = createContext();
1921

2022
export function DockerContextProvider({ children }) {
21-
const [labspaces, setLabspaces] = useState(null);
23+
const { catalogs } = useCatalogs();
24+
2225
const [additionalLabspaces, setAdditionalLabspaces] = useState(
2326
localStorage.getItem("custom-labspaces")
2427
? JSON.parse(localStorage.getItem("custom-labspaces"))
@@ -34,25 +37,6 @@ export function DockerContextProvider({ children }) {
3437

3538
const logProcessor = useMemo(() => new LogProcessor(), []);
3639

37-
useEffect(() => {
38-
Promise.all(
39-
CATALOGS.map((url) =>
40-
fetch(url)
41-
.then((res) => res.text())
42-
.then((text) => parse(text))
43-
.then((data) => data.labspaces || [])
44-
.then((labs) => labs.map(l => ({...l, catalog: url}))),
45-
),
46-
).then((results) => {
47-
setLabspaces(results.flat()
48-
.sort((a, b) => {
49-
if (a.highlighted && !b.highlighted) return -1;
50-
if (!a.highlighted && b.highlighted) return 1;
51-
return a.title.localeCompare(b.title);
52-
}));
53-
});
54-
}, []);
55-
5640
useEffect(() => {
5741
function checkIfRunning() {
5842
ddClient.docker.cli
@@ -133,39 +117,6 @@ export function DockerContextProvider({ children }) {
133117
[setHasLabspace, setStartingLabspace, setForceRefreshCount, setLaunchLog],
134118
);
135119

136-
const addLabspace = useCallback(
137-
(title, publishedRepo) => {
138-
const newLabspace = { title, publishedRepo };
139-
setAdditionalLabspaces((labspaces) => [...labspaces, newLabspace]);
140-
},
141-
[setAdditionalLabspaces],
142-
);
143-
144-
const removeLabspace = useCallback(
145-
(publishedRepo) => {
146-
setAdditionalLabspaces((labs) =>
147-
labs.filter((l) => l.publishedRepo !== publishedRepo),
148-
);
149-
},
150-
[setAdditionalLabspaces],
151-
);
152-
153-
useEffect(() => {
154-
localStorage.setItem(
155-
"custom-labspaces",
156-
JSON.stringify(additionalLabspaces),
157-
);
158-
}, [additionalLabspaces]);
159-
160-
if (!labspaces) {
161-
return (
162-
<div className="mt-5 text-center">
163-
<Spinner />
164-
<p>Loading...</p>
165-
</div>
166-
);
167-
}
168-
169120
return (
170121
<DockerContext.Provider
171122
value={{
@@ -178,13 +129,6 @@ export function DockerContextProvider({ children }) {
178129
startLabspace,
179130
startingLabspace,
180131
launchLog,
181-
182-
highlightedLabspaces: labspaces
183-
.filter((l) => l.highlighted)
184-
.slice(0, 3),
185-
labspaces: [...additionalLabspaces, ...labspaces.slice(3)],
186-
addLabspace,
187-
removeLabspace,
188132
}}
189133
>
190134
{children}

0 commit comments

Comments
 (0)