mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-09-24 17:05:45 +00:00
feat: implement 'run command' button to execute arbitrary restic commands in a repo
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Plan } from "../../gen/ts/v1/config_pb";
|
||||
import { Flex, Tabs, Tooltip, Typography } from "antd";
|
||||
import { Button, Flex, Tabs, Tooltip, Typography } from "antd";
|
||||
import { useAlertApi } from "../components/Alerts";
|
||||
import { OperationList } from "../components/OperationList";
|
||||
import { OperationTree } from "../components/OperationTree";
|
||||
@@ -9,9 +9,11 @@ import { backrestService } from "../api";
|
||||
import { GetOperationsRequest } from "../../gen/ts/v1/service_pb";
|
||||
import { SpinButton } from "../components/SpinButton";
|
||||
import { shouldHideStatus } from "../state/oplog";
|
||||
import { useShowModal } from "../components/ModalManager";
|
||||
|
||||
export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
|
||||
const alertsApi = useAlertApi()!;
|
||||
const showModal = useShowModal();
|
||||
|
||||
const handleBackupNow = async () => {
|
||||
try {
|
||||
@@ -77,6 +79,14 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
|
||||
Clear Error History
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Open a restic shell to run commands on the repository.">
|
||||
<Button type="default" onClick={async () => {
|
||||
const { RunCommandModal } = await import("./RunCommandModal");
|
||||
showModal(<RunCommandModal repoId={plan.repo!} />);
|
||||
}}>
|
||||
Run Command
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Repo } from "../../gen/ts/v1/config_pb";
|
||||
import { Col, Empty, Flex, Row, Spin, TabsProps, Tabs, Tooltip, Typography } from "antd";
|
||||
import { Col, Empty, Flex, Row, Spin, TabsProps, Tabs, Tooltip, Typography, Button } from "antd";
|
||||
import { OperationList } from "../components/OperationList";
|
||||
import { OperationTree } from "../components/OperationTree";
|
||||
import { MAX_OPERATION_HISTORY, STATS_OPERATION_HISTORY } from "../constants";
|
||||
@@ -14,10 +14,12 @@ import { SpinButton } from "../components/SpinButton";
|
||||
import { useConfig } from "../components/ConfigProvider";
|
||||
import { useAlertApi } from "../components/Alerts";
|
||||
import { LineChart } from "@mui/x-charts";
|
||||
import { useShowModal } from "../components/ModalManager";
|
||||
|
||||
|
||||
export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
const [config, setConfig] = useConfig();
|
||||
const showModal = useShowModal();
|
||||
|
||||
// Task handlers
|
||||
const handleIndexNow = async () => {
|
||||
@@ -99,6 +101,14 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
Compute Stats
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Open a restic shell to run commands on the repository.">
|
||||
<Button type="default" onClick={async () => {
|
||||
const { RunCommandModal } = await import("./RunCommandModal");
|
||||
showModal(<RunCommandModal repoId={repo.id!} />);
|
||||
}}>
|
||||
Run Command
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Tabs
|
||||
defaultActiveKey={items[0].key}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Button, Input, Modal, Space } from "antd";
|
||||
import React from "react";
|
||||
import { useShowModal } from "../components/ModalManager";
|
||||
import { backrestService } from "../api";
|
||||
import { SpinButton } from "../components/SpinButton";
|
||||
import { ConnectError } from "@connectrpc/connect";
|
||||
|
||||
|
||||
interface Invocation {
|
||||
command: string;
|
||||
output: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
|
||||
export const RunCommandModal = ({ repoId }: { repoId: string }) => {
|
||||
const showModal = useShowModal();
|
||||
const [command, setCommand] = React.useState("");
|
||||
const [running, setRunning] = React.useState(false);
|
||||
const [invocations, setInvocations] = React.useState<Invocation[]>([]);
|
||||
|
||||
const handleCancel = () => {
|
||||
showModal(null);
|
||||
}
|
||||
|
||||
const doExecute = async () => {
|
||||
if (running) return;
|
||||
setRunning(true);
|
||||
|
||||
const newInvocation = { command, output: "", error: "" };
|
||||
setInvocations((invocations) => [newInvocation, ...invocations]);
|
||||
|
||||
let segments: string[] = [];
|
||||
|
||||
try {
|
||||
for await (const bytes of backrestService.runCommand({ repoId, command })) {
|
||||
const output = new TextDecoder("utf-8").decode(bytes.value);
|
||||
segments.push(output);
|
||||
setInvocations((invocations) => {
|
||||
const copy = [...invocations];
|
||||
copy[0] = {
|
||||
...copy[0],
|
||||
output: segments.join(""),
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
setInvocations((invocations) => {
|
||||
const copy = [...invocations];
|
||||
copy[0] = {
|
||||
...copy[0],
|
||||
error: (e as Error).message,
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
} finally {
|
||||
setRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
return <Modal
|
||||
open={true}
|
||||
onCancel={handleCancel}
|
||||
title={"Run Command in repo " + repoId}
|
||||
width="80vw"
|
||||
footer={[]}
|
||||
>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input placeholder="Run a restic comamnd e.g. 'help' to print help text" onChange={(e) => setCommand(e.target.value)} onKeyUp={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
doExecute();
|
||||
}
|
||||
}} />
|
||||
<SpinButton type="primary" onClickAsync={doExecute}>Execute</SpinButton>
|
||||
</Space.Compact>
|
||||
{invocations.map((invocation, i) => <div key={i}>
|
||||
{invocation.output ? <pre>{invocation.output}</pre> : null}
|
||||
{invocation.error ? <pre style={{ color: "red" }}>{invocation.error}</pre> : null}
|
||||
</div>)}
|
||||
</Modal>
|
||||
}
|
||||
Reference in New Issue
Block a user