mirror of
https://github.com/containrrr/watchtower.git
synced 2025-12-17 23:50:13 +01:00
Fix Codacy issues
This commit is contained in:
parent
5645cd23fe
commit
741f315f14
19 changed files with 163 additions and 135 deletions
|
|
@ -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 {
|
func (api *API) EnableCors(fn http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,18 @@ import (
|
||||||
"github.com/containrrr/watchtower/pkg/types"
|
"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 {
|
type Handler struct {
|
||||||
Path string
|
Path string
|
||||||
Client container.Client
|
Client container.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckRequest defines the type for the request data of the Check endpoint
|
type checkRequest struct {
|
||||||
type CheckRequest struct {
|
ContainerID string
|
||||||
ContainerId string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckResponse defines the type for the response data of the Check endpoint
|
type checkResponse struct {
|
||||||
type CheckResponse struct {
|
ContainerID string
|
||||||
ContainerId string
|
|
||||||
HasUpdate bool
|
HasUpdate bool
|
||||||
NewVersion string
|
NewVersion string
|
||||||
NewVersionCreated 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.")
|
log.Info("Check for update triggered by HTTP API request.")
|
||||||
|
|
||||||
var request CheckRequest
|
var request checkRequest
|
||||||
err := json.NewDecoder(r.Body).Decode(&request)
|
err := json.NewDecoder(r.Body).Decode(&request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
|
|
@ -57,7 +55,7 @@ func (handle *Handler) HandlePost(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
client := handle.Client
|
client := handle.Client
|
||||||
container, err := client.GetContainer(types.ContainerID(request.ContainerId))
|
container, err := client.GetContainer(types.ContainerID(request.ContainerID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
@ -74,8 +72,8 @@ func (handle *Handler) HandlePost(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := CheckResponse{
|
data := checkResponse{
|
||||||
ContainerId: request.ContainerId,
|
ContainerID: request.ContainerID,
|
||||||
HasUpdate: stale,
|
HasUpdate: stale,
|
||||||
NewVersion: newestImage.ShortID(),
|
NewVersion: newestImage.ShortID(),
|
||||||
NewVersionCreated: created,
|
NewVersionCreated: created,
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,8 @@ type Handler struct {
|
||||||
Client container.Client
|
Client container.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerListEntry defines the type of each container in the response
|
type containerListEntry struct {
|
||||||
type ContainerListEntry struct {
|
ContainerID string
|
||||||
ContainerId string
|
|
||||||
ContainerName string
|
ContainerName string
|
||||||
ImageName string
|
ImageName string
|
||||||
ImageNameShort string
|
ImageNameShort string
|
||||||
|
|
@ -27,9 +26,8 @@ type ContainerListEntry struct {
|
||||||
ImageCreatedDate string
|
ImageCreatedDate string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListResponse defines the return type of the List endpoint
|
type listResponse struct {
|
||||||
type ListResponse struct {
|
Containers []containerListEntry
|
||||||
Containers []ContainerListEntry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New is a factory function creating a new List instance
|
// 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()))
|
w.Write([]byte(err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
data := ListResponse{Containers: []ContainerListEntry{}}
|
data := listResponse{Containers: []containerListEntry{}}
|
||||||
|
|
||||||
for _, c := range containers {
|
for _, c := range containers {
|
||||||
data.Containers = append(data.Containers, ContainerListEntry{
|
data.Containers = append(data.Containers, containerListEntry{
|
||||||
ContainerId: c.ID().ShortID(),
|
ContainerID: c.ID().ShortID(),
|
||||||
ContainerName: c.Name()[1:],
|
ContainerName: c.Name()[1:],
|
||||||
ImageName: c.ImageName(),
|
ImageName: c.ImageName(),
|
||||||
ImageNameShort: strings.Split(c.ImageName(), ":")[0],
|
ImageNameShort: strings.Split(c.ImageName(), ":")[0],
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ func (d *Dashboard) Start() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dashboard) templatedHttpHandler(h http.Handler) http.HandlerFunc {
|
func (d *Dashboard) templatedHTTPHandler(h http.Handler) http.HandlerFunc {
|
||||||
const apiUrlTemplate = "%s://%s:%s/%s/"
|
const apiURLTemplate = "%s://%s:%s/%s/"
|
||||||
indexTemplate, err := template.ParseFiles(d.rootDir + "/index.html")
|
indexTemplate, err := template.ParseFiles(d.rootDir + "/index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error when parsing index template")
|
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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path == "/" {
|
if r.URL.Path == "/" {
|
||||||
hostName := strings.Split(r.Host, ":")[0]
|
hostName := strings.Split(r.Host, ":")[0]
|
||||||
apiUrl := fmt.Sprintf(apiUrlTemplate, d.apiScheme, hostName, d.apiPort, d.apiVersion)
|
apiURL := fmt.Sprintf(apiURLTemplate, d.apiScheme, hostName, d.apiPort, d.apiVersion)
|
||||||
err = indexTemplate.Execute(w, struct{ ApiUrl string }{
|
err = indexTemplate.Execute(w, struct{ APIURL string }{
|
||||||
ApiUrl: apiUrl,
|
APIURL: apiURL,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error when executing index template")
|
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 {
|
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() {
|
func (d *Dashboard) runHTTPServer() {
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
VITE_API_URL={{.ApiUrl}}
|
VITE_API_URL={{.APIURL}}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react";
|
||||||
import ContainerView from "./components/ContainerView";
|
import ContainerView from "./components/ContainerView";
|
||||||
import Header from "./components/Header";
|
import Header from "./components/Header";
|
||||||
import Login from "./components/Login";
|
import Login from "./components/Login";
|
||||||
|
|
@ -20,7 +20,7 @@ const App = () => {
|
||||||
const onLogIn = () => {
|
const onLogIn = () => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
const onLogOut = () => {
|
const onLogOut = () => {
|
||||||
logOut();
|
logOut();
|
||||||
|
|
@ -44,4 +44,4 @@ const App = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import ContainerEntry from "../models/ContainerEntry";
|
import ContainerModel from "../models/ContainerModel";
|
||||||
import ContainerListEntry from "./ContainerListEntry";
|
import ContainerListEntry from "./ContainerListEntry";
|
||||||
|
|
||||||
interface ContainerListProps {
|
interface ContainerListProps {
|
||||||
containers: ContainerEntry[];
|
containers: ContainerModel[];
|
||||||
onContainerClick: (container: ContainerEntry) => void;
|
onContainerClick: (container: ContainerModel) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContainerList = (props: ContainerListProps) => (
|
const ContainerList = (props: ContainerListProps) => (
|
||||||
<ul className="list-group">
|
<ul className="list-group">
|
||||||
{props.containers.map(c => <ContainerListEntry {...c} onClick={() => props.onContainerClick(c)} />)}
|
{props.containers.map((c) => <ContainerListEntry {...c} onClick={() => props.onContainerClick(c)} />)}
|
||||||
</ul >
|
</ul >
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import ContainerEntry from "../models/ContainerEntry";
|
import ContainerModel from "../models/ContainerModel";
|
||||||
import ImageInfo from "./ImageInfo";
|
import ImageInfo from "./ImageInfo";
|
||||||
import SpinnerGrow from "./SpinnerGrow";
|
import SpinnerGrow from "./SpinnerGrow";
|
||||||
|
|
||||||
const ContainerListEntry = (props: (ContainerEntry & { onClick: () => void })) => (
|
interface ContainerListEntryProps extends ContainerModel {
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContainerListEntry = (props: ContainerListEntryProps) => (
|
||||||
<li className="list-group-item d-flex justify-content-between align-items-center container-list-entry" onClick={props.onClick} role="button">
|
<li className="list-group-item d-flex justify-content-between align-items-center container-list-entry" onClick={props.onClick} role="button">
|
||||||
<div className="ms-1 me-3 container-list-entry-icon">
|
<div className="ms-1 me-3 container-list-entry-icon">
|
||||||
{props.Selected
|
{props.Selected
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ContainerEntry from "../models/ContainerEntry"
|
import ContainerModel from "../models/ContainerModel";
|
||||||
import { check, list, update } from "../services/Api";
|
import { check, list, update } from "../services/Api";
|
||||||
import ContainerList from "./ContainerList";
|
import ContainerList from "./ContainerList";
|
||||||
import Spinner from "./Spinner";
|
import Spinner from "./Spinner";
|
||||||
import SpinnerModal from "./SpinnerModal";
|
import SpinnerModal from "./SpinnerModal";
|
||||||
|
import { UpdateSelected, UpdateAll, UpdateCheck } from "./UpdateButtons";
|
||||||
|
|
||||||
interface ViewModel {
|
interface ViewModel {
|
||||||
Containers: ContainerEntry[];
|
Containers: ContainerModel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContainerView = () => {
|
const ContainerView = () => {
|
||||||
|
|
@ -16,32 +17,28 @@ const ContainerView = () => {
|
||||||
const [hasChecked, setHasChecked] = useState(false);
|
const [hasChecked, setHasChecked] = useState(false);
|
||||||
const [viewModel, setViewModel] = useState<ViewModel>({ Containers: [] });
|
const [viewModel, setViewModel] = useState<ViewModel>({ Containers: [] });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
listContainers();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const containers = viewModel.Containers;
|
const containers = viewModel.Containers;
|
||||||
const containersWithUpdates = containers.filter(c => c.HasUpdate);
|
const containersWithUpdates = containers.filter((c) => c.HasUpdate);
|
||||||
const containersWithoutUpdates = containers.filter(c => !c.HasUpdate);
|
const containersWithoutUpdates = containers.filter((c) => !c.HasUpdate);
|
||||||
const hasSelectedContainers = containers.some(c => c.Selected);
|
const hasSelectedContainers = containers.some((c) => c.Selected);
|
||||||
const hasUpdates = containersWithUpdates.length > 0;
|
const hasUpdates = containersWithUpdates.length > 0;
|
||||||
|
|
||||||
const checkForUpdates = async () => {
|
const checkForUpdates = async () => {
|
||||||
setChecking(true);
|
setChecking(true);
|
||||||
|
|
||||||
setViewModel(m => ({
|
setViewModel((m) => ({
|
||||||
...m,
|
...m,
|
||||||
Containers: m.Containers.map(c => ({
|
Containers: m.Containers.map((c) => ({
|
||||||
...c,
|
...c,
|
||||||
IsChecking: true
|
IsChecking: true
|
||||||
}))
|
}))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await Promise.all(containers.map(async (c1) => {
|
await Promise.all(containers.map(async (c1) => {
|
||||||
const result = await check(c1.ContainerId);
|
const result = await check(c1.ContainerID);
|
||||||
setViewModel(m => ({
|
setViewModel((m) => ({
|
||||||
...m,
|
...m,
|
||||||
Containers: m.Containers.map((c2, i) => (c1.ContainerId === c2.ContainerId ? {
|
Containers: m.Containers.map((c2) => (c1.ContainerID === c2.ContainerID ? {
|
||||||
...c2,
|
...c2,
|
||||||
...result,
|
...result,
|
||||||
IsChecking: false
|
IsChecking: false
|
||||||
|
|
@ -57,7 +54,17 @@ const ContainerView = () => {
|
||||||
const listContainers = async () => {
|
const listContainers = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await list();
|
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);
|
setLoading(false);
|
||||||
setHasChecked(false);
|
setHasChecked(false);
|
||||||
};
|
};
|
||||||
|
|
@ -75,14 +82,14 @@ const ContainerView = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSelected = async () => {
|
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);
|
await updateImages(selectedImages);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onContainerClick = (container: ContainerEntry) => {
|
const onContainerClick = (container: ContainerModel) => {
|
||||||
setViewModel(m => ({
|
setViewModel((m) => ({
|
||||||
...m,
|
...m,
|
||||||
Containers: m.Containers.map(c2 => (container.ContainerId === c2.ContainerId ? {
|
Containers: m.Containers.map((c2) => (container.ContainerID === c2.ContainerID ? {
|
||||||
...c2,
|
...c2,
|
||||||
Selected: !c2.Selected,
|
Selected: !c2.Selected,
|
||||||
} : c2
|
} : c2
|
||||||
|
|
@ -90,6 +97,10 @@ const ContainerView = () => {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listContainers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mt-5 p-5 d-block">
|
<main className="mt-5 p-5 d-block">
|
||||||
<SpinnerModal visible={updating} title="Updating containers" message="Please wait..." />
|
<SpinnerModal visible={updating} title="Updating containers" message="Please wait..." />
|
||||||
|
|
@ -127,27 +138,4 @@ const ContainerView = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface UpdateButtonProps {
|
|
||||||
disabled: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UpdateSelected = (props: UpdateButtonProps) => (
|
|
||||||
<button type="button" className="btn btn-primary me-2" disabled={props.disabled} onClick={props.onClick}>
|
|
||||||
<i className="bi bi-arrow-down-circle me-2"></i>Update selected
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
const UpdateAll = (props: UpdateButtonProps) => (
|
|
||||||
<button type="button" className="btn btn-primary me-2" disabled={props.disabled} onClick={props.onClick}>
|
|
||||||
<i className="bi bi-arrow-down-circle me-2"></i>Update all
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
const UpdateCheck = (props: UpdateButtonProps) => (
|
|
||||||
<button type="button" className="btn btn-outline-primary" disabled={props.disabled} onClick={props.onClick}>
|
|
||||||
<i className="bi bi-arrow-repeat me-2"></i>Check for updates
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ContainerView;
|
export default ContainerView;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import logo from "../assets/logo.png"
|
import logo from "../assets/logo.png";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
onLogOut: () => void;
|
onLogOut: () => void;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import { ChangeEvent, FormEvent, useState } from "react";
|
import { ChangeEvent, FormEvent, useState } from "react";
|
||||||
import { logIn } from "../services/Api";
|
import { logIn } from "../services/Api";
|
||||||
import logo from "../assets/logo.png"
|
import logo from "../assets/logo.png";
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
onLogin: () => void;
|
onLogin: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Login = (props: LoginProps) => {
|
const Login = (props: LoginProps) => {
|
||||||
const [password, setPassword] = useState<string | undefined>(undefined);
|
const [password, setPassword] = useState("");
|
||||||
const [remember, setRemember] = useState(false);
|
const [remember, setRemember] = useState(false);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (event.target.type === "checkbox") {
|
if (event.target.type === "checkbox") {
|
||||||
|
|
@ -20,10 +20,10 @@ const Login = (props: LoginProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
setError(undefined);
|
setError("");
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (password === undefined) {
|
if (password === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import logo from "../assets/logo.png"
|
import logo from "../assets/logo.png";
|
||||||
|
|
||||||
interface SpinnerModalProps {
|
interface SpinnerModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
|
@ -10,9 +10,11 @@ interface SpinnerModalProps {
|
||||||
const SpinnerModal = (props: SpinnerModalProps) => {
|
const SpinnerModal = (props: SpinnerModalProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.classList.toggle("modal-open", props.visible === true);
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -37,7 +39,7 @@ const SpinnerModal = (props: SpinnerModalProps) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SpinnerModal;
|
export default SpinnerModal;
|
||||||
23
web/src/components/UpdateButtons.tsx
Normal file
23
web/src/components/UpdateButtons.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
interface UpdateButtonProps {
|
||||||
|
disabled: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateSelected = (props: UpdateButtonProps) => (
|
||||||
|
<button type="button" className="btn btn-primary me-2" disabled={props.disabled} onClick={props.onClick}>
|
||||||
|
<i className="bi bi-arrow-down-circle me-2"></i>Update selected
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateAll = (props: UpdateButtonProps) => (
|
||||||
|
<button type="button" className="btn btn-primary me-2" disabled={props.disabled} onClick={props.onClick}>
|
||||||
|
<i className="bi bi-arrow-down-circle me-2"></i>Update all
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateCheck = (props: UpdateButtonProps) => (
|
||||||
|
<button type="button" className="btn btn-outline-primary" disabled={props.disabled} onClick={props.onClick}>
|
||||||
|
<i className="bi bi-arrow-repeat me-2"></i>Check for updates
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
@ -1,45 +1,43 @@
|
||||||
[data-md-color-scheme="containrrr"] {
|
[data-md-color-scheme="containrrr"] {
|
||||||
/* Primary and accent */
|
/* primary and accent */
|
||||||
--md-primary-fg-color: #406170;
|
--md-primary-fg-color: #406170;
|
||||||
--md-primary-fg-color--light: #acbfc7;
|
--md-primary-fg-color--light: #acbfc7;
|
||||||
--md-primary-fg-color--dark: #003343;
|
--md-primary-fg-color--dark: #003343;
|
||||||
--md-accent-fg-color: #003343;
|
--md-accent-fg-color: #003343;
|
||||||
--md-accent-fg-color--transparent: #00334310;
|
--md-accent-fg-color--transparent: #00334310;
|
||||||
|
|
||||||
/* Typeset overrides */
|
/* typeset overrides */
|
||||||
--md-typeset-a-color: var(--md-primary-fg-color);
|
--md-typeset-a-color: var(--md-primary-fg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bs-primary: #406170 !important;
|
--bs-primary: #406170 !important;
|
||||||
--bs-primary-rgb: 3, 140, 127;
|
--bs-primary-rgb: 3, 140, 127;
|
||||||
|
|
||||||
--bs-secondary: #acbfc7 !important;
|
--bs-secondary: #acbfc7 !important;
|
||||||
--bs-secondary-rgb: 64, 97, 112;
|
--bs-secondary-rgb: 64, 97, 112;
|
||||||
|
|
||||||
--bs-dark: #003343 !important;
|
--bs-dark: #003343 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
--bs-btn-bg: #038C7F;
|
--bs-btn-bg: #038c7f;
|
||||||
--bs-btn-border-color: ##038C7F;
|
--bs-btn-border-color: ##038c7f;
|
||||||
--bs-btn-hover-bg: #02675d;
|
--bs-btn-hover-bg: #02675d;
|
||||||
--bs-btn-hover-border-color: #025a52;
|
--bs-btn-hover-border-color: #025a52;
|
||||||
--bs-btn-active-bg: #025a52;
|
--bs-btn-active-bg: #025a52;
|
||||||
--bs-btn-active-border-color: #025a52;
|
--bs-btn-active-border-color: #025a52;
|
||||||
--bs-btn-disabled-bg: #038C7F;
|
--bs-btn-disabled-bg: #038c7f;
|
||||||
--bs-btn-disabled-border-color: #038C7F;
|
--bs-btn-disabled-border-color: #038c7f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline-primary {
|
.btn-outline-primary {
|
||||||
--bs-btn-color: #038C7F;
|
--bs-btn-color: #038c7f;
|
||||||
--bs-btn-disabled-color: #038C7F;
|
--bs-btn-disabled-color: #038c7f;
|
||||||
--bs-btn-border-color: #038C7F;
|
--bs-btn-border-color: #038c7f;
|
||||||
--bs-btn-hover-bg: #02675d;
|
--bs-btn-hover-bg: #02675d;
|
||||||
--bs-btn-hover-border-color: #025a52;
|
--bs-btn-hover-border-color: #025a52;
|
||||||
--bs-btn-active-bg: #025a52;
|
--bs-btn-active-bg: #025a52;
|
||||||
--bs-btn-active-border-color: #025a52;
|
--bs-btn-active-border-color: #025a52;
|
||||||
--bs-btn-disabled-border-color: #038C7F;
|
--bs-btn-disabled-border-color: #038c7f;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
@ -48,5 +46,5 @@ body {
|
||||||
|
|
||||||
.container-list-entry:hover .container-list-entry-icon .bi-box::before {
|
.container-list-entry:hover .container-list-entry-icon .bi-box::before {
|
||||||
/* .bi-box-fill */
|
/* .bi-box-fill */
|
||||||
content: "\F7D2"
|
content: "\F7D2";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import React from "react"
|
import { StrictMode } from "react";
|
||||||
import ReactDOM from "react-dom/client"
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App"
|
import App from "./App";
|
||||||
|
|
||||||
import "bootstrap/dist/css/bootstrap.min.css";
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||||
import "./main.css";
|
import "./main.css";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</StrictMode>
|
||||||
)
|
);
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
6
web/src/models/ContainerModel.ts
Normal file
6
web/src/models/ContainerModel.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { CheckResponse, ContainerListEntry } from "../services/Api";
|
||||||
|
|
||||||
|
export default interface ContainerModel extends ContainerListEntry, CheckResponse {
|
||||||
|
Selected: boolean;
|
||||||
|
IsChecking: boolean;
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ const headers = () => ({
|
||||||
"Authorization": "Bearer " + token
|
"Authorization": "Bearer " + token
|
||||||
});
|
});
|
||||||
|
|
||||||
export const logIn = async (password: string, remember: boolean) => {
|
export const logIn = async (password: string, remember: boolean): Promise<boolean> => {
|
||||||
token = password;
|
token = password;
|
||||||
const response = await fetch(apiBasePath + "list", {
|
const response = await fetch(apiBasePath + "list", {
|
||||||
headers: headers()
|
headers: headers()
|
||||||
|
|
@ -33,7 +33,7 @@ export const logIn = async (password: string, remember: boolean) => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkLogin = async () => {
|
export const checkLogin = async (): Promise<boolean> => {
|
||||||
const savedToken = localStorage.getItem(tokenStorageKey);
|
const savedToken = localStorage.getItem(tokenStorageKey);
|
||||||
if (savedToken) {
|
if (savedToken) {
|
||||||
return await logIn(savedToken, false);
|
return await logIn(savedToken, false);
|
||||||
|
|
@ -47,17 +47,17 @@ export const logOut = () => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const list = async () => {
|
export const list = async (): Promise<ListResponse> => {
|
||||||
const response = await fetch(apiBasePath + "list", {
|
const response = await fetch(apiBasePath + "list", {
|
||||||
headers: headers()
|
headers: headers()
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data as ListResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const check = async (containerId: string) => {
|
export const check = async (containerId: string): Promise<CheckResponse> => {
|
||||||
const requestData = {
|
const requestData: CheckRequest = {
|
||||||
ContainerId: containerId
|
ContainerID: containerId
|
||||||
};
|
};
|
||||||
const response = await fetch(apiBasePath + "check", {
|
const response = await fetch(apiBasePath + "check", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -68,14 +68,14 @@ export const check = async (containerId: string) => {
|
||||||
body: JSON.stringify(requestData)
|
body: JSON.stringify(requestData)
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data as CheckResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const update = async (images?: string[]) => {
|
export const update = async (images?: string[]): Promise<boolean> => {
|
||||||
let updateUrl = new URL(apiBasePath + "/update");
|
let updateUrl = new URL(apiBasePath + "/update");
|
||||||
|
|
||||||
if (images instanceof Array) {
|
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(), {
|
const response = await fetch(updateUrl.toString(), {
|
||||||
|
|
@ -83,4 +83,28 @@ export const update = async (images?: string[]) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.ok;
|
return response.ok;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineConfig, loadEnv } from 'vite'
|
import { defineConfig, loadEnv } from "vite"
|
||||||
import react from '@vitejs/plugin-react'
|
import react from "@vitejs/plugin-react"
|
||||||
|
|
||||||
const htmlPlugin = (mode: string) => {
|
const htmlPlugin = (mode: string) => {
|
||||||
const env = loadEnv(mode, ".");
|
const env = loadEnv(mode, ".");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue