mirror of
https://github.com/amir20/dozzle.git
synced 2026-01-03 03:27:29 +01:00
feat: can start and stop containers now using the drop down. This feature is not enabled by default. (#2548)
This commit is contained in:
3
assets/auto-imports.d.ts
vendored
3
assets/auto-imports.d.ts
vendored
@@ -183,6 +183,7 @@ declare global {
|
||||
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useContainerActions: typeof import('./composable/containerActions')['useContainerActions']
|
||||
const useContainerContext: typeof import('./composable/containerContext')['useContainerContext']
|
||||
const useContainerStore: typeof import('./stores/container')['useContainerStore']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
@@ -533,6 +534,7 @@ declare module 'vue' {
|
||||
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
|
||||
readonly useContainerContext: UnwrapRef<typeof import('./composable/containerContext')['useContainerContext']>
|
||||
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
|
||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||
@@ -876,6 +878,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||
readonly useContainerActions: UnwrapRef<typeof import('./composable/containerActions')['useContainerActions']>
|
||||
readonly useContainerContext: UnwrapRef<typeof import('./composable/containerContext')['useContainerContext']>
|
||||
readonly useContainerStore: UnwrapRef<typeof import('./stores/container')['useContainerStore']>
|
||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||
|
||||
3
assets/components.d.ts
vendored
3
assets/components.d.ts
vendored
@@ -12,8 +12,11 @@ declare module 'vue' {
|
||||
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
|
||||
'Carbon:information': typeof import('~icons/carbon/information')['default']
|
||||
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
|
||||
'Carbon:play': typeof import('~icons/carbon/play')['default']
|
||||
'Carbon:restart': typeof import('~icons/carbon/restart')['default']
|
||||
'Carbon:star': typeof import('~icons/carbon/star')['default']
|
||||
'Carbon:starFilled': typeof import('~icons/carbon/star-filled')['default']
|
||||
'Carbon:stopFilledAlt': typeof import('~icons/carbon/stop-filled-alt')['default']
|
||||
'Carbon:warning': typeof import('~icons/carbon/warning')['default']
|
||||
'Cil:checkCircle': typeof import('~icons/cil/check-circle')['default']
|
||||
'Cil:circle': typeof import('~icons/cil/circle')['default']
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<carbon:circle-solid class="w-2.5 text-red" v-if="streamConfig.stderr" />
|
||||
<carbon:circle-solid class="w-2.5 text-blue" v-if="streamConfig.stdout" />
|
||||
</label>
|
||||
<ul tabindex="0" class="menu dropdown-content rounded-box z-50 w-52 bg-base p-1 shadow">
|
||||
<ul tabindex="0" class="menu dropdown-content z-50 w-52 rounded-box bg-base p-1 shadow">
|
||||
<li>
|
||||
<a @click.prevent="clear()">
|
||||
<octicon:trash-24 /> {{ $t("toolbar.clear") }}
|
||||
@@ -65,17 +65,57 @@
|
||||
{{ $t("toolbar.show", { std: "STDERR" }) }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Container Actions (Enabled via config) -->
|
||||
<template v-if="enableActions">
|
||||
<li class="line"></li>
|
||||
<li>
|
||||
<button
|
||||
@click="stop()"
|
||||
:disabled="actionStates.stop || actionStates.restart"
|
||||
v-if="container.state == 'running'"
|
||||
>
|
||||
<carbon:stop-filled-alt /> {{ $t("toolbar.stop") }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="start()"
|
||||
:disabled="actionStates.start || actionStates.restart"
|
||||
v-if="container.state != 'running'"
|
||||
>
|
||||
<carbon:play /> {{ $t("toolbar.start") }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="restart()" :disabled="disableRestart">
|
||||
<carbon:restart
|
||||
:class="{
|
||||
'animate-spin': actionStates.restart,
|
||||
'text-secondary': actionStates.restart,
|
||||
}"
|
||||
/>
|
||||
{{ $t("toolbar.restart") }}
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { showSearch } = useSearchFilter();
|
||||
const { base } = config;
|
||||
const { base, enableActions } = config;
|
||||
|
||||
const clear = defineEmit();
|
||||
|
||||
const { container, streamConfig } = useContainerContext();
|
||||
|
||||
// container context is provided in the parent component: <LogContainer>
|
||||
const { actionStates, start, stop, restart } = useContainerActions();
|
||||
|
||||
const disableRestart = computed(() => {
|
||||
return actionStates.stop || actionStates.start || actionStates.restart;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
|
||||
45
assets/composable/containerActions.ts
Normal file
45
assets/composable/containerActions.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
type ContainerActions = "start" | "stop" | "restart";
|
||||
export const useContainerActions = () => {
|
||||
const { container } = useContainerContext();
|
||||
const { showToast } = useToast();
|
||||
|
||||
const actionStates = reactive({
|
||||
stop: false,
|
||||
restart: false,
|
||||
start: false,
|
||||
});
|
||||
|
||||
async function actionHandler(action: ContainerActions) {
|
||||
const actionUrl = `/api/actions/${action}/${container.value.host}/${container.value.id}`;
|
||||
|
||||
const errors = {
|
||||
404: "container not found",
|
||||
500: "unable to complete action",
|
||||
400: "invalid action",
|
||||
} as Record<number, string>;
|
||||
|
||||
const defaultError = "something went wrong";
|
||||
const toastTitle = "Action Failed";
|
||||
|
||||
actionStates[action] = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(withBase(actionUrl), { method: "POST" });
|
||||
if (!response.ok) {
|
||||
const message = errors[response.status] ?? defaultError;
|
||||
showToast({ type: "error", message, title: toastTitle });
|
||||
}
|
||||
} catch (error) {
|
||||
showToast({ type: "error", message: defaultError, title: toastTitle });
|
||||
}
|
||||
|
||||
actionStates[action] = false;
|
||||
}
|
||||
|
||||
return {
|
||||
actionStates,
|
||||
start: () => actionHandler("start"),
|
||||
stop: () => actionHandler("stop"),
|
||||
restart: () => actionHandler("restart"),
|
||||
};
|
||||
};
|
||||
@@ -11,6 +11,7 @@ export interface Config {
|
||||
hostname: string;
|
||||
hosts: { name: string; id: string }[];
|
||||
authProvider: "simple" | "none" | "forward-proxy";
|
||||
enableActions: boolean;
|
||||
user?: {
|
||||
username: string;
|
||||
email: string;
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
@@ -49,6 +50,9 @@ type DockerCLI interface {
|
||||
ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error)
|
||||
ContainerStats(ctx context.Context, containerID string, stream bool) (types.ContainerStats, error)
|
||||
Ping(ctx context.Context) (types.Ping, error)
|
||||
ContainerStart(ctx context.Context, containerID string, options types.ContainerStartOptions) error
|
||||
ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error
|
||||
ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
@@ -145,6 +149,19 @@ func (d *Client) FindContainer(id string) (Container, error) {
|
||||
return container, nil
|
||||
}
|
||||
|
||||
func (d *Client) ContainerActions(action string, id string) error {
|
||||
switch action {
|
||||
case "start":
|
||||
return d.cli.ContainerStart(context.Background(), id, types.ContainerStartOptions{})
|
||||
case "stop":
|
||||
return d.cli.ContainerStop(context.Background(), id, container.StopOptions{})
|
||||
case "restart":
|
||||
return d.cli.ContainerRestart(context.Background(), id, container.StopOptions{})
|
||||
default:
|
||||
return fmt.Errorf("unknown action: %s", action)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Client) ListContainers() ([]Container, error) {
|
||||
containerListOptions := types.ContainerListOptions{
|
||||
Filters: d.filters,
|
||||
|
||||
40
internal/web/container_actions.go
Normal file
40
internal/web/container_actions.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
|
||||
action := chi.URLParam(r, "action")
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
log.Debugf("container action: %s, container id: %s", action, id)
|
||||
|
||||
client := h.clientFromRequest(r)
|
||||
|
||||
if client == nil {
|
||||
log.Errorf("no client found for host %v", r.URL)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
container, err := client.FindContainer(id)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
err = client.ContainerActions(action, container.ID)
|
||||
if err != nil {
|
||||
log.Errorf("error while trying to perform action: %s", action)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("container action performed: %s; container id: %s", action, id)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
@@ -51,6 +51,7 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
"hostname": h.config.Hostname,
|
||||
"hosts": hosts,
|
||||
"authProvider": h.config.Authorization.Provider,
|
||||
"enableActions": h.config.EnableActions,
|
||||
}
|
||||
|
||||
pages, err := content.ReadAll()
|
||||
|
||||
@@ -35,6 +35,7 @@ type Config struct {
|
||||
NoAnalytics bool
|
||||
Dev bool
|
||||
Authorization Authorization
|
||||
EnableActions bool
|
||||
}
|
||||
|
||||
type Authorization struct {
|
||||
@@ -63,6 +64,7 @@ type DockerClient interface {
|
||||
ContainerStats(context.Context, string, chan<- docker.ContainerStat) error
|
||||
Ping(context.Context) (types.Ping, error)
|
||||
Host() *docker.Host
|
||||
ContainerActions(action string, id string) error
|
||||
}
|
||||
|
||||
func CreateServer(clients map[string]DockerClient, content fs.FS, config Config) *http.Server {
|
||||
@@ -104,6 +106,7 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Get("/api/logs/download/{host}/{id}", h.downloadLogs)
|
||||
r.Get("/api/logs/{host}/{id}", h.fetchLogsBetweenDates)
|
||||
r.Get("/api/events/stream", h.streamEvents)
|
||||
r.Post("/api/actions/{action}/{host}/{id}", h.containerActions)
|
||||
r.Get("/api/releases", h.releases)
|
||||
r.Patch("/api/profile", h.updateProfile)
|
||||
r.Get("/api/content/{id}", h.staticContent)
|
||||
|
||||
@@ -4,6 +4,9 @@ toolbar:
|
||||
search: Search
|
||||
show: Show only {std}
|
||||
show-all: Show all streams
|
||||
stop: Stop
|
||||
start: Start
|
||||
restart: Restart
|
||||
label:
|
||||
containers: Containers
|
||||
total-containers: Total Containers
|
||||
|
||||
2
main.go
2
main.go
@@ -53,6 +53,7 @@ type args struct {
|
||||
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running."`
|
||||
RemoteHost []string `arg:"env:DOZZLE_REMOTE_HOST,--remote-host,separate" help:"list of hosts to connect remotely"`
|
||||
AuthProvider string `arg:"--auth-provider,env:DOZZLE_AUTH_PROVIDER" default:"none" help:"sets the auth provider to use. Currently only forward-proxy is supported."`
|
||||
EnableActions bool `arg:"env:DOZZLE_ENABLE_ACTIONS" default:"false" help:"enables essential actions on containers from the web interface."`
|
||||
}
|
||||
|
||||
type HealthcheckCmd struct {
|
||||
@@ -212,6 +213,7 @@ func createServer(args args, clients map[string]web.DockerClient) *http.Server {
|
||||
Provider: provider,
|
||||
Authorizer: authorizer,
|
||||
},
|
||||
EnableActions: args.EnableActions,
|
||||
}
|
||||
|
||||
assets, err := fs.Sub(content, "dist")
|
||||
|
||||
Reference in New Issue
Block a user