diff --git a/pkg/api/api.go b/pkg/api/api.go
index 88ec288..ac36e3b 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -23,6 +23,7 @@ func New(token string) *API {
}
}
+// EnableCors is a middleware that enables CORS for the API
func (api *API) EnableCors(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
diff --git a/pkg/api/check/check.go b/pkg/api/check/check.go
index b0e83fb..d5e7c9f 100644
--- a/pkg/api/check/check.go
+++ b/pkg/api/check/check.go
@@ -10,20 +10,18 @@ import (
"github.com/containrrr/watchtower/pkg/types"
)
-// Handler is an HTTP handle for serving list data
+// Handler is an HTTP handle for serving check data
type Handler struct {
Path string
Client container.Client
}
-// CheckRequest defines the type for the request data of the Check endpoint
-type CheckRequest struct {
- ContainerId string
+type checkRequest struct {
+ ContainerID string
}
-// CheckResponse defines the type for the response data of the Check endpoint
-type CheckResponse struct {
- ContainerId string
+type checkResponse struct {
+ ContainerID string
HasUpdate bool
NewVersion string
NewVersionCreated string
@@ -47,7 +45,7 @@ func (handle *Handler) HandlePost(w http.ResponseWriter, r *http.Request) {
log.Info("Check for update triggered by HTTP API request.")
- var request CheckRequest
+ var request checkRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil {
log.Error(err)
@@ -57,7 +55,7 @@ func (handle *Handler) HandlePost(w http.ResponseWriter, r *http.Request) {
}
client := handle.Client
- container, err := client.GetContainer(types.ContainerID(request.ContainerId))
+ container, err := client.GetContainer(types.ContainerID(request.ContainerID))
if err != nil {
log.Error(err)
w.WriteHeader(http.StatusInternalServerError)
@@ -74,8 +72,8 @@ func (handle *Handler) HandlePost(w http.ResponseWriter, r *http.Request) {
return
}
- data := CheckResponse{
- ContainerId: request.ContainerId,
+ data := checkResponse{
+ ContainerID: request.ContainerID,
HasUpdate: stale,
NewVersion: newestImage.ShortID(),
NewVersionCreated: created,
diff --git a/pkg/api/list/list.go b/pkg/api/list/list.go
index 06d79e1..53288dd 100644
--- a/pkg/api/list/list.go
+++ b/pkg/api/list/list.go
@@ -17,9 +17,8 @@ type Handler struct {
Client container.Client
}
-// ContainerListEntry defines the type of each container in the response
-type ContainerListEntry struct {
- ContainerId string
+type containerListEntry struct {
+ ContainerID string
ContainerName string
ImageName string
ImageNameShort string
@@ -27,9 +26,8 @@ type ContainerListEntry struct {
ImageCreatedDate string
}
-// ListResponse defines the return type of the List endpoint
-type ListResponse struct {
- Containers []ContainerListEntry
+type listResponse struct {
+ Containers []containerListEntry
}
// New is a factory function creating a new List instance
@@ -59,11 +57,11 @@ func (handle *Handler) HandleGet(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(err.Error()))
}
- data := ListResponse{Containers: []ContainerListEntry{}}
+ data := listResponse{Containers: []containerListEntry{}}
for _, c := range containers {
- data.Containers = append(data.Containers, ContainerListEntry{
- ContainerId: c.ID().ShortID(),
+ data.Containers = append(data.Containers, containerListEntry{
+ ContainerID: c.ID().ShortID(),
ContainerName: c.Name()[1:],
ImageName: c.ImageName(),
ImageNameShort: strings.Split(c.ImageName(), ":")[0],
diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go
index 9f65296..72e8d82 100644
--- a/pkg/dashboard/dashboard.go
+++ b/pkg/dashboard/dashboard.go
@@ -41,8 +41,8 @@ func (d *Dashboard) Start() error {
return nil
}
-func (d *Dashboard) templatedHttpHandler(h http.Handler) http.HandlerFunc {
- const apiUrlTemplate = "%s://%s:%s/%s/"
+func (d *Dashboard) templatedHTTPHandler(h http.Handler) http.HandlerFunc {
+ const apiURLTemplate = "%s://%s:%s/%s/"
indexTemplate, err := template.ParseFiles(d.rootDir + "/index.html")
if err != nil {
log.Error("Error when parsing index template")
@@ -53,9 +53,9 @@ func (d *Dashboard) templatedHttpHandler(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
hostName := strings.Split(r.Host, ":")[0]
- apiUrl := fmt.Sprintf(apiUrlTemplate, d.apiScheme, hostName, d.apiPort, d.apiVersion)
- err = indexTemplate.Execute(w, struct{ ApiUrl string }{
- ApiUrl: apiUrl,
+ apiURL := fmt.Sprintf(apiURLTemplate, d.apiScheme, hostName, d.apiPort, d.apiVersion)
+ err = indexTemplate.Execute(w, struct{ APIURL string }{
+ APIURL: apiURL,
})
if err != nil {
log.Error("Error when executing index template")
@@ -70,7 +70,7 @@ func (d *Dashboard) templatedHttpHandler(h http.Handler) http.HandlerFunc {
}
func (d *Dashboard) getHandler() http.Handler {
- return d.templatedHttpHandler(http.FileServer(http.Dir(d.rootDir)))
+ return d.templatedHTTPHandler(http.FileServer(http.Dir(d.rootDir)))
}
func (d *Dashboard) runHTTPServer() {
diff --git a/web/.env.production b/web/.env.production
index f23b4a2..506f0c9 100644
--- a/web/.env.production
+++ b/web/.env.production
@@ -1 +1 @@
-VITE_API_URL={{.ApiUrl}}
\ No newline at end of file
+VITE_API_URL={{.APIURL}}
\ No newline at end of file
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 90360a3..237323a 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react"
+import { useEffect, useState } from "react";
import ContainerView from "./components/ContainerView";
import Header from "./components/Header";
import Login from "./components/Login";
@@ -20,7 +20,7 @@ const App = () => {
const onLogIn = () => {
setLoading(false);
setLoggedIn(true);
- }
+ };
const onLogOut = () => {
logOut();
@@ -44,4 +44,4 @@ const App = () => {
);
};
-export default App
+export default App;
\ No newline at end of file
diff --git a/web/src/components/ContainerList.tsx b/web/src/components/ContainerList.tsx
index ff0b73e..7428750 100644
--- a/web/src/components/ContainerList.tsx
+++ b/web/src/components/ContainerList.tsx
@@ -1,14 +1,14 @@
-import ContainerEntry from "../models/ContainerEntry";
+import ContainerModel from "../models/ContainerModel";
import ContainerListEntry from "./ContainerListEntry";
interface ContainerListProps {
- containers: ContainerEntry[];
- onContainerClick: (container: ContainerEntry) => void;
+ containers: ContainerModel[];
+ onContainerClick: (container: ContainerModel) => void;
}
const ContainerList = (props: ContainerListProps) => (
- {props.containers.map(c => props.onContainerClick(c)} />)}
+ {props.containers.map((c) => props.onContainerClick(c)} />)}
);
diff --git a/web/src/components/ContainerListEntry.tsx b/web/src/components/ContainerListEntry.tsx
index 4725833..631fcf1 100644
--- a/web/src/components/ContainerListEntry.tsx
+++ b/web/src/components/ContainerListEntry.tsx
@@ -1,8 +1,12 @@
-import ContainerEntry from "../models/ContainerEntry";
+import ContainerModel from "../models/ContainerModel";
import ImageInfo from "./ImageInfo";
import SpinnerGrow from "./SpinnerGrow";
-const ContainerListEntry = (props: (ContainerEntry & { onClick: () => void })) => (
+interface ContainerListEntryProps extends ContainerModel {
+ onClick: () => void;
+}
+
+const ContainerListEntry = (props: ContainerListEntryProps) => (
{props.Selected
diff --git a/web/src/components/ContainerView.tsx b/web/src/components/ContainerView.tsx
index 2e8a304..7723a46 100644
--- a/web/src/components/ContainerView.tsx
+++ b/web/src/components/ContainerView.tsx
@@ -1,12 +1,13 @@
import { useEffect, useState } from "react";
-import ContainerEntry from "../models/ContainerEntry"
+import ContainerModel from "../models/ContainerModel";
import { check, list, update } from "../services/Api";
import ContainerList from "./ContainerList";
import Spinner from "./Spinner";
import SpinnerModal from "./SpinnerModal";
+import { UpdateSelected, UpdateAll, UpdateCheck } from "./UpdateButtons";
interface ViewModel {
- Containers: ContainerEntry[];
+ Containers: ContainerModel[];
}
const ContainerView = () => {
@@ -16,32 +17,28 @@ const ContainerView = () => {
const [hasChecked, setHasChecked] = useState(false);
const [viewModel, setViewModel] = useState
({ Containers: [] });
- useEffect(() => {
- listContainers();
- }, []);
-
const containers = viewModel.Containers;
- const containersWithUpdates = containers.filter(c => c.HasUpdate);
- const containersWithoutUpdates = containers.filter(c => !c.HasUpdate);
- const hasSelectedContainers = containers.some(c => c.Selected);
+ const containersWithUpdates = containers.filter((c) => c.HasUpdate);
+ const containersWithoutUpdates = containers.filter((c) => !c.HasUpdate);
+ const hasSelectedContainers = containers.some((c) => c.Selected);
const hasUpdates = containersWithUpdates.length > 0;
const checkForUpdates = async () => {
setChecking(true);
- setViewModel(m => ({
+ setViewModel((m) => ({
...m,
- Containers: m.Containers.map(c => ({
+ Containers: m.Containers.map((c) => ({
...c,
IsChecking: true
}))
}));
await Promise.all(containers.map(async (c1) => {
- const result = await check(c1.ContainerId);
- setViewModel(m => ({
+ const result = await check(c1.ContainerID);
+ setViewModel((m) => ({
...m,
- Containers: m.Containers.map((c2, i) => (c1.ContainerId === c2.ContainerId ? {
+ Containers: m.Containers.map((c2) => (c1.ContainerID === c2.ContainerID ? {
...c2,
...result,
IsChecking: false
@@ -57,7 +54,17 @@ const ContainerView = () => {
const listContainers = async () => {
setLoading(true);
const data = await list();
- setViewModel(data);
+ setViewModel({
+ Containers: data.Containers.map((c) => ({
+ ...c,
+ Selected: false,
+ IsChecking: false,
+ HasUpdate: false,
+ IsUpdating: false,
+ NewVersion: "",
+ NewVersionCreated: ""
+ }))
+ });
setLoading(false);
setHasChecked(false);
};
@@ -75,14 +82,14 @@ const ContainerView = () => {
};
const updateSelected = async () => {
- const selectedImages = containers.filter(c => c.Selected === true).map(c => c.ImageNameShort);
+ const selectedImages = containers.filter((c) => c.Selected === true).map((c) => c.ImageNameShort);
await updateImages(selectedImages);
};
- const onContainerClick = (container: ContainerEntry) => {
- setViewModel(m => ({
+ const onContainerClick = (container: ContainerModel) => {
+ setViewModel((m) => ({
...m,
- Containers: m.Containers.map(c2 => (container.ContainerId === c2.ContainerId ? {
+ Containers: m.Containers.map((c2) => (container.ContainerID === c2.ContainerID ? {
...c2,
Selected: !c2.Selected,
} : c2
@@ -90,6 +97,10 @@ const ContainerView = () => {
}));
};
+ useEffect(() => {
+ listContainers();
+ }, []);
+
return (
@@ -127,27 +138,4 @@ const ContainerView = () => {
);
};
-interface UpdateButtonProps {
- disabled: boolean;
- onClick: () => void;
-};
-
-const UpdateSelected = (props: UpdateButtonProps) => (
-
-);
-
-const UpdateAll = (props: UpdateButtonProps) => (
-
-);
-
-const UpdateCheck = (props: UpdateButtonProps) => (
-
-);
-
export default ContainerView;
\ No newline at end of file
diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx
index 1c3baa1..0aa0d8b 100644
--- a/web/src/components/Header.tsx
+++ b/web/src/components/Header.tsx
@@ -1,4 +1,4 @@
-import logo from "../assets/logo.png"
+import logo from "../assets/logo.png";
interface HeaderProps {
onLogOut: () => void;
diff --git a/web/src/components/Login.tsx b/web/src/components/Login.tsx
index ef6ceba..72e2823 100644
--- a/web/src/components/Login.tsx
+++ b/web/src/components/Login.tsx
@@ -1,15 +1,15 @@
import { ChangeEvent, FormEvent, useState } from "react";
import { logIn } from "../services/Api";
-import logo from "../assets/logo.png"
+import logo from "../assets/logo.png";
interface LoginProps {
onLogin: () => void;
}
const Login = (props: LoginProps) => {
- const [password, setPassword] = useState(undefined);
+ const [password, setPassword] = useState("");
const [remember, setRemember] = useState(false);
- const [error, setError] = useState(undefined);
+ const [error, setError] = useState("");
const handleChange = (event: ChangeEvent) => {
if (event.target.type === "checkbox") {
@@ -20,10 +20,10 @@ const Login = (props: LoginProps) => {
};
const handleSubmit = async (event: FormEvent) => {
- setError(undefined);
+ setError("");
event.preventDefault();
- if (password === undefined) {
+ if (password === "") {
return;
}
diff --git a/web/src/components/SpinnerModal.tsx b/web/src/components/SpinnerModal.tsx
index 6719d78..d89dd9e 100644
--- a/web/src/components/SpinnerModal.tsx
+++ b/web/src/components/SpinnerModal.tsx
@@ -1,5 +1,5 @@
import { useEffect } from "react";
-import logo from "../assets/logo.png"
+import logo from "../assets/logo.png";
interface SpinnerModalProps {
visible: boolean;
@@ -10,9 +10,11 @@ interface SpinnerModalProps {
const SpinnerModal = (props: SpinnerModalProps) => {
useEffect(() => {
document.body.classList.toggle("modal-open", props.visible === true);
- }, [props.visible])
+ }, [props.visible]);
- if (props.visible !== true) return null;
+ if (props.visible !== true) {
+ return null;
+ }
return (
@@ -37,7 +39,7 @@ const SpinnerModal = (props: SpinnerModalProps) => {
- )
+ );
};
export default SpinnerModal;
\ No newline at end of file
diff --git a/web/src/components/UpdateButtons.tsx b/web/src/components/UpdateButtons.tsx
new file mode 100644
index 0000000..47ca6f0
--- /dev/null
+++ b/web/src/components/UpdateButtons.tsx
@@ -0,0 +1,23 @@
+
+interface UpdateButtonProps {
+ disabled: boolean;
+ onClick: () => void;
+}
+
+export const UpdateSelected = (props: UpdateButtonProps) => (
+
+);
+
+export const UpdateAll = (props: UpdateButtonProps) => (
+
+);
+
+export const UpdateCheck = (props: UpdateButtonProps) => (
+
+);
\ No newline at end of file
diff --git a/web/src/main.css b/web/src/main.css
index a7c2bc7..94066ae 100644
--- a/web/src/main.css
+++ b/web/src/main.css
@@ -1,45 +1,43 @@
[data-md-color-scheme="containrrr"] {
- /* Primary and accent */
+ /* primary and accent */
--md-primary-fg-color: #406170;
--md-primary-fg-color--light: #acbfc7;
--md-primary-fg-color--dark: #003343;
--md-accent-fg-color: #003343;
--md-accent-fg-color--transparent: #00334310;
- /* Typeset overrides */
+ /* typeset overrides */
--md-typeset-a-color: var(--md-primary-fg-color);
}
:root {
--bs-primary: #406170 !important;
--bs-primary-rgb: 3, 140, 127;
-
--bs-secondary: #acbfc7 !important;
--bs-secondary-rgb: 64, 97, 112;
-
--bs-dark: #003343 !important;
}
.btn-primary {
- --bs-btn-bg: #038C7F;
- --bs-btn-border-color: ##038C7F;
+ --bs-btn-bg: #038c7f;
+ --bs-btn-border-color: ##038c7f;
--bs-btn-hover-bg: #02675d;
--bs-btn-hover-border-color: #025a52;
--bs-btn-active-bg: #025a52;
--bs-btn-active-border-color: #025a52;
- --bs-btn-disabled-bg: #038C7F;
- --bs-btn-disabled-border-color: #038C7F;
+ --bs-btn-disabled-bg: #038c7f;
+ --bs-btn-disabled-border-color: #038c7f;
}
.btn-outline-primary {
- --bs-btn-color: #038C7F;
- --bs-btn-disabled-color: #038C7F;
- --bs-btn-border-color: #038C7F;
+ --bs-btn-color: #038c7f;
+ --bs-btn-disabled-color: #038c7f;
+ --bs-btn-border-color: #038c7f;
--bs-btn-hover-bg: #02675d;
--bs-btn-hover-border-color: #025a52;
--bs-btn-active-bg: #025a52;
--bs-btn-active-border-color: #025a52;
- --bs-btn-disabled-border-color: #038C7F;
+ --bs-btn-disabled-border-color: #038c7f;
}
body {
@@ -48,5 +46,5 @@ body {
.container-list-entry:hover .container-list-entry-icon .bi-box::before {
/* .bi-box-fill */
- content: "\F7D2"
-}
\ No newline at end of file
+ content: "\F7D2";
+}
diff --git a/web/src/main.tsx b/web/src/main.tsx
index a9fef4d..0cc528d 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -1,13 +1,13 @@
-import React from "react"
-import ReactDOM from "react-dom/client"
-import App from "./App"
+import { StrictMode } from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap-icons/font/bootstrap-icons.css";
import "./main.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
-
+
-
-)
\ No newline at end of file
+
+);
\ No newline at end of file
diff --git a/web/src/models/ContainerEntry.ts b/web/src/models/ContainerEntry.ts
deleted file mode 100644
index 3d94a8b..0000000
--- a/web/src/models/ContainerEntry.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-
-export default interface ContainerEntry {
- ContainerId: string;
- ContainerName: string;
- ImageName: string;
- ImageNameShort: string;
- ImageVersion: string;
- ImageCreatedDate: string;
- NewVersion: string;
- NewVersionCreated: string;
- HasUpdate: boolean;
- Selected: boolean;
- IsChecking: boolean;
-}
\ No newline at end of file
diff --git a/web/src/models/ContainerModel.ts b/web/src/models/ContainerModel.ts
new file mode 100644
index 0000000..9bc36f1
--- /dev/null
+++ b/web/src/models/ContainerModel.ts
@@ -0,0 +1,6 @@
+import { CheckResponse, ContainerListEntry } from "../services/Api";
+
+export default interface ContainerModel extends ContainerListEntry, CheckResponse {
+ Selected: boolean;
+ IsChecking: boolean;
+}
\ No newline at end of file
diff --git a/web/src/services/Api.ts b/web/src/services/Api.ts
index 008fc6a..fd601d9 100644
--- a/web/src/services/Api.ts
+++ b/web/src/services/Api.ts
@@ -16,7 +16,7 @@ const headers = () => ({
"Authorization": "Bearer " + token
});
-export const logIn = async (password: string, remember: boolean) => {
+export const logIn = async (password: string, remember: boolean): Promise => {
token = password;
const response = await fetch(apiBasePath + "list", {
headers: headers()
@@ -33,7 +33,7 @@ export const logIn = async (password: string, remember: boolean) => {
return false;
};
-export const checkLogin = async () => {
+export const checkLogin = async (): Promise => {
const savedToken = localStorage.getItem(tokenStorageKey);
if (savedToken) {
return await logIn(savedToken, false);
@@ -47,17 +47,17 @@ export const logOut = () => {
localStorage.clear();
};
-export const list = async () => {
+export const list = async (): Promise => {
const response = await fetch(apiBasePath + "list", {
headers: headers()
});
const data = await response.json();
- return data;
+ return data as ListResponse;
};
-export const check = async (containerId: string) => {
- const requestData = {
- ContainerId: containerId
+export const check = async (containerId: string): Promise => {
+ const requestData: CheckRequest = {
+ ContainerID: containerId
};
const response = await fetch(apiBasePath + "check", {
method: "POST",
@@ -68,14 +68,14 @@ export const check = async (containerId: string) => {
body: JSON.stringify(requestData)
});
const data = await response.json();
- return data;
+ return data as CheckResponse;
};
-export const update = async (images?: string[]) => {
+export const update = async (images?: string[]): Promise => {
let updateUrl = new URL(apiBasePath + "/update");
if (images instanceof Array) {
- images.map(image => updateUrl.searchParams.append("image", image));
+ images.map((image) => updateUrl.searchParams.append("image", image));
}
const response = await fetch(updateUrl.toString(), {
@@ -83,4 +83,28 @@ export const update = async (images?: string[]) => {
});
return response.ok;
-};
\ No newline at end of file
+};
+
+export interface ListResponse {
+ Containers: ContainerListEntry[];
+}
+
+export interface ContainerListEntry {
+ ContainerID: string;
+ ContainerName: string;
+ ImageName: string;
+ ImageNameShort: string;
+ ImageVersion: string;
+ ImageCreatedDate: string;
+}
+
+export interface CheckRequest {
+ ContainerID: string;
+}
+
+export interface CheckResponse {
+ ContainerID: string;
+ HasUpdate: boolean;
+ NewVersion: string;
+ NewVersionCreated: string;
+}
\ No newline at end of file
diff --git a/web/vite.config.ts b/web/vite.config.ts
index bd44117..3fca177 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -1,5 +1,5 @@
-import { defineConfig, loadEnv } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig, loadEnv } from "vite"
+import react from "@vitejs/plugin-react"
const htmlPlugin = (mode: string) => {
const env = loadEnv(mode, ".");