diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..8427297
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/favicon.png b/public/favicon.png
deleted file mode 100644
index fc9336e..0000000
Binary files a/public/favicon.png and /dev/null differ
diff --git a/public/index.html b/public/index.html
index b3c9cf9..8e25233 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2,7 +2,7 @@
-
+
diff --git a/src/components/ContainerUpLearnMore.js b/src/components/ContainerUpLearnMore.js
new file mode 100644
index 0000000..8506cd9
--- /dev/null
+++ b/src/components/ContainerUpLearnMore.js
@@ -0,0 +1,21 @@
+import {Alert} from "@mui/material";
+
+export default function ContainerUpLearnMore({variant}) {
+ if (!process.env.REACT_APP_CONTAINERUP_DEMO) {
+ return '';
+ }
+
+ if (variant === "long") {
+ return (
+
+ Love this project? Learn more about ContainerUp here.
+
+ );
+ }
+
+ return (
+
+ Learn more about ContainerUp here.
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/MyDrawerItem.js b/src/components/MyDrawerItem.js
index 7144994..7ef26ff 100644
--- a/src/components/MyDrawerItem.js
+++ b/src/components/MyDrawerItem.js
@@ -4,13 +4,17 @@ import ListItemText from "@mui/material/ListItemText";
import ListItem from "@mui/material/ListItem";
import {useLocation, useNavigate} from "react-router-dom";
-export default function MyDrawerItem({drawerOpen, text, icon, path, or}) {
+export default function MyDrawerItem({drawerOpen, text, icon, path, or, href}) {
const navigate = useNavigate();
const {pathname} = useLocation();
const handleClick = () => {
- navigate(path)
- }
+ if (href) {
+ window.open(href, '_blank').focus();
+ return;
+ }
+ navigate(path);
+ };
let selected = false;
if (pathname.indexOf(path) === 0
diff --git a/src/components/MyTerminal.js b/src/components/MyTerminal.js
deleted file mode 100644
index 92cfa80..0000000
--- a/src/components/MyTerminal.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import {useEffect, useRef} from "react";
-import {Box} from "@mui/material";
-import {Terminal} from "xterm";
-import { FitAddon } from 'xterm-addon-fit';
-
-export default function MyTerminal({writerOnReceive}) {
- const ref = useRef();
-
- useEffect(() => {
- if (!writerOnReceive) {
- return () => {};
- }
-
- const xterm = new Terminal({
- fontSize: 13
- });
- const fitAddon = new FitAddon()
- xterm.loadAddon(fitAddon);
- xterm.open(ref.current);
- fitAddon.fit();
- writerOnReceive(data => xterm.write(data));
-
- const handleResize = () => {
- fitAddon.fit();
- };
- window.addEventListener('resize', handleResize);
-
- return () => {
- writerOnReceive(null);
- xterm.dispose();
- window.removeEventListener('resize', handleResize);
- };
- });
-
- return (
-
-
- );
-}
\ No newline at end of file
diff --git a/src/components/MyTerminalTwoWay.js b/src/components/MyTerminalTwoWay.js
index 617e12a..f9702e3 100644
--- a/src/components/MyTerminalTwoWay.js
+++ b/src/components/MyTerminalTwoWay.js
@@ -39,7 +39,6 @@ export default function MyTerminalTwoWay({dataSide}) {
default:
console.log('unknown dataSide command', data);
}
- xterm.write(data)
});
xterm.onData(str => dsWriter({type: 'data', data: str}));
xterm.onResize(({cols, rows}) => reportSize({cols, rows}));
diff --git a/src/components/WebsocketConnectError.js b/src/components/WebsocketConnectError.js
new file mode 100644
index 0000000..d824d86
--- /dev/null
+++ b/src/components/WebsocketConnectError.js
@@ -0,0 +1,9 @@
+import {Alert} from "@mui/material";
+
+export default function WebsocketConnectError() {
+ return (
+
+ Failed to connect websocket.
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/WebsocketDisconnectError.js b/src/components/WebsocketDisconnectError.js
new file mode 100644
index 0000000..b24fc6d
--- /dev/null
+++ b/src/components/WebsocketDisconnectError.js
@@ -0,0 +1,22 @@
+import {enqueueSnackbar} from "notistack";
+import {Button} from "@mui/material";
+import RefreshIcon from '@mui/icons-material/Refresh';
+
+export function showWebsocketDisconnectError() {
+ const action = snackbarId => (
+ }
+ onClick={() => window.location.reload()}
+ >
+ Reload
+
+ );
+
+ enqueueSnackbar('Websocket disconnected. The information on this page is NOT up-to-date.', {
+ variant: 'warning',
+ persist: true,
+ action
+ });
+}
diff --git a/src/lib/dataProvidor.js b/src/lib/dataProvidor.js
index e7f3ee6..470fe9f 100644
--- a/src/lib/dataProvidor.js
+++ b/src/lib/dataProvidor.js
@@ -43,7 +43,7 @@ const makeAioConnection = (onData, onOpen, onError, onClose) => {
ws.close();
state = 99;
- onError(new Error('Websocket error'));
+ onError(new WebsocketError('Cannot connect to websocket', websocketErrorTypeConnect));
});
ws.addEventListener('close', event => {
if (state === 99) {
@@ -57,7 +57,7 @@ const makeAioConnection = (onData, onOpen, onError, onClose) => {
if (code === 4001) {
err = dataModel.errors.errNoLogin;
} else {
- err = new Error(`Websocket closed ${code} ${reason}`);
+ err = new WebsocketError(`Websocket closed ${code} ${reason}`, websocketErrorTypeDisconnect);
}
onClose(err);
});
@@ -147,7 +147,7 @@ const aioMain = onAioClose => {
return () => commonUnsubscribe(index, fnUnsub);
}
if (closed) {
- onError(new Error('connection closed'));
+ onError(new WebsocketError('connection closed', websocketErrorTypeDisconnect));
return () => {};
}
@@ -230,3 +230,28 @@ export function aioProvider() {
});
return aio;
}
+
+class WebsocketError extends Error {
+ constructor(message, type) {
+ super(message);
+ this.name = this.constructor.name;
+ this.errType = type;
+ }
+}
+
+const websocketErrorTypeDisconnect = 'disconnect';
+const websocketErrorTypeConnect = 'connect';
+
+export function isDisconnectError(err) {
+ if (!(err instanceof WebsocketError)) {
+ return false;
+ }
+ return err.errType === websocketErrorTypeDisconnect;
+}
+
+export function isConnectError(err) {
+ if (!(err instanceof WebsocketError)) {
+ return false;
+ }
+ return err.errType === websocketErrorTypeConnect;
+}
diff --git a/src/lib/ga4.js b/src/lib/ga4.js
new file mode 100644
index 0000000..8d3ea32
--- /dev/null
+++ b/src/lib/ga4.js
@@ -0,0 +1,42 @@
+import { useEffect } from "react";
+import { useLocation } from "react-router-dom";
+
+export const useGA4 = () => {
+ const ga4 = process.env.REACT_APP_CONTAINERUP_GA4;
+
+ useEffect(() => {
+ if (!ga4) {
+ return;
+ }
+
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){window.dataLayer.push(arguments);}
+ gtag('js', new Date());
+ gtag('config', ga4, {
+ send_page_view: false
+ });
+
+ const script = document.createElement('script');
+ script.src = "https://www.googletagmanager.com/gtag/js?id=" + ga4;
+ script.async = true;
+
+ document.body.appendChild(script);
+ return () => {
+ document.body.removeChild(script);
+ };
+ }, [ga4]);
+
+ const location = useLocation();
+ useEffect(() => {
+ if (!ga4) {
+ return;
+ }
+
+ function gtag(){window.dataLayer.push(arguments);}
+ gtag("event", "page_view", {
+ page_path: location.pathname + location.search + location.hash,
+ page_search: location.search,
+ page_hash: location.hash,
+ });
+ }, [ga4, location]);
+};
diff --git a/src/routes/Containers/ContainerDetail.js b/src/routes/Containers/ContainerDetail.js
index b3ee718..9ebcc45 100644
--- a/src/routes/Containers/ContainerDetail.js
+++ b/src/routes/Containers/ContainerDetail.js
@@ -2,8 +2,9 @@ import {Box, Tab, Tabs} from "@mui/material";
import {Link, Outlet, useLocation, useNavigate, useParams} from "react-router-dom";
import {useEffect, useMemo, useState} from "react";
import {getController} from "../../lib/HostGuestController";
-import {aioProvider} from "../../lib/dataProvidor";
+import {aioProvider, isDisconnectError} from "../../lib/dataProvidor";
import dataModel from "../../lib/dataModel";
+import {showWebsocketDisconnectError} from "../../components/WebsocketDisconnectError";
const tabs = [{
to: "overview",
@@ -112,13 +113,21 @@ export default function ContainerDetail() {
if (error.response) {
e = error.response.data;
}
- setErrMsg(e);
- setLoading(false);
+ if (loading) {
+ setErrMsg(e);
+ setLoading(false);
+ } else {
+ if (isDisconnectError(error)) {
+ showWebsocketDisconnectError();
+ } else {
+ setErrMsg(e);
+ }
+ }
};
const cancel = aioProvider().container(containerId, onData, onError)
return () => cancel();
- }, [containerId, navigate, pathname]);
+ }, [containerId, loading, navigate, pathname]);
return (
<>
diff --git a/src/routes/Containers/ContainerDetailButtons.js b/src/routes/Containers/ContainerDetailButtons.js
index 25c9199..98bd6cc 100644
--- a/src/routes/Containers/ContainerDetailButtons.js
+++ b/src/routes/Containers/ContainerDetailButtons.js
@@ -6,7 +6,7 @@ import SaveIcon from "@mui/icons-material/Save";
import ListItemText from "@mui/material/ListItemText";
import {green, orange, red, yellow} from "@mui/material/colors";
import DeleteIcon from "@mui/icons-material/Delete";
-import {useEffect, useState} from "react";
+import {useCallback, useEffect, useState} from "react";
import StopIcon from "@mui/icons-material/Stop";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import {getController} from "../../lib/HostGuestController";
@@ -192,6 +192,10 @@ export default function ContainerDetailButtons() {
return () => ac.abort();
}, [actioning, container, navigate, pathname, action]);
+ const handleDialogCommitClose = useCallback(() => {
+ setDialogCommitOpen(false)
+ }, []);
+
const st = getStatusText(container);
if (!container) {
@@ -205,7 +209,8 @@ export default function ContainerDetailButtons() {
{container && (
<>
setDialogRemoveOpen(false)}
@@ -214,8 +219,9 @@ export default function ContainerDetailButtons() {
setDialogCommitOpen(false)}
+ containerName={container.Name}
+ containerIdShort={container.Id.substring(0, 12)}
+ onClose={handleDialogCommitClose}
/>
>
)}
diff --git a/src/routes/Containers/Create/ContainerCreate.js b/src/routes/Containers/Create/ContainerCreate.js
index c39f6a0..fb29f63 100644
--- a/src/routes/Containers/Create/ContainerCreate.js
+++ b/src/routes/Containers/Create/ContainerCreate.js
@@ -172,7 +172,7 @@ export default function ContainerCreate() {
return {
container: v.container,
host: v.host,
- rw: v.rw
+ readWrite: v.rw
};
}),
ports: ports.map(p => {
@@ -195,7 +195,6 @@ export default function ContainerCreate() {
return;
}
- console.log(error)
let errStr = error.toString();
if (dataModel.errIsNoLogin(error)) {
errStr = 'Session expired. Reload the page, and try again.';
diff --git a/src/routes/Containers/Create/CreateNameImage.js b/src/routes/Containers/Create/CreateNameImage.js
index 251f6b0..1a0b7bb 100644
--- a/src/routes/Containers/Create/CreateNameImage.js
+++ b/src/routes/Containers/Create/CreateNameImage.js
@@ -15,6 +15,7 @@ import ImagePullTerminal from "../../Images/List/ImagePullTerminal";
import CreateImagePullActions from "./CreateImagePullActions";
import Pipe from "../../../lib/Pipe";
import CheckIcon from "@mui/icons-material/Check";
+import {demoImage} from "../../Images/List/ImagePullDialog";
const checkNameAndGetImage = (containerName, imageId, abortController) => {
const p1 = dataModel.containerInspect(containerName, false, abortController)
@@ -103,10 +104,22 @@ export default function CreateNameImage({name, image, onConfirm, onEdited}) {
const pullTerminationOnReceive = pullTerminationPipe.useOnReceive();
const pullTerminationWriter = pullTerminationPipe.useWriter();
- const imgPulled = imageName && imageOpt && (
- (imageOpt.Names && imageOpt.Names[0] && imageName === imageOpt.Names[0]) ||
- (imageOpt.Id && imageName === imageOpt.Id.substring(0, 12))
- );
+ let imgPulled = false;
+ if (imageName && imageOpt) {
+ if (imageOpt.Names) {
+ for (const name of imageOpt.Names) {
+ if (name === imageName) {
+ imgPulled = true;
+ break;
+ }
+ }
+ }
+ if (!imgPulled) {
+ if (imageOpt.Id && (imageName === imageOpt.Id.substring(0, 12) || imageOpt.Id === imageName)) {
+ imgPulled = true;
+ }
+ }
+ }
const [loadingImageDetail, setLoadingImageDetail] = useState(false);
@@ -384,6 +397,10 @@ export default function CreateNameImage({name, image, onConfirm, onEdited}) {
<>
The image {imageName} is not found locally.
To continue, pull this image first. Pull the image now?
+
+
+ As a limit of the demo server, please try this one: {demoImage}
+
>
)}
diff --git a/src/routes/Containers/List/ContainerActions.js b/src/routes/Containers/List/ContainerActions.js
index 38a72d5..6f96c45 100644
--- a/src/routes/Containers/List/ContainerActions.js
+++ b/src/routes/Containers/List/ContainerActions.js
@@ -13,7 +13,7 @@ import TerminalIcon from "@mui/icons-material/Terminal";
import DeleteIcon from "@mui/icons-material/Delete";
import MoreVertIcon from '@mui/icons-material/MoreVert';
import SaveIcon from '@mui/icons-material/Save';
-import {useEffect, useState} from "react";
+import {useCallback, useEffect, useState} from "react";
import dataModel from "../../../lib/dataModel";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
@@ -141,10 +141,15 @@ export default function ContainerActions({c}) {
const canExec = !actioning && (c.State === 'running');
const canDelete = !actioning && (c.State === 'exited' || c.State === 'created');
+ const handleDialogCommitClose = useCallback(() => {
+ setDialogCommitOpen(false)
+ }, []);
+
return (
<>
setDialogRemoveOpen(false)}
@@ -153,8 +158,9 @@ export default function ContainerActions({c}) {
setDialogCommitOpen(false)}
+ containerName={c.Names[0]}
+ containerIdShort={c.idShort}
+ onClose={handleDialogCommitClose}
/>
{canStart ? (
diff --git a/src/routes/Containers/List/ContainerDialogCommit.js b/src/routes/Containers/List/ContainerDialogCommit.js
index b9ad2b6..abbc9d2 100644
--- a/src/routes/Containers/List/ContainerDialogCommit.js
+++ b/src/routes/Containers/List/ContainerDialogCommit.js
@@ -6,21 +6,22 @@ import {
DialogContentText,
DialogTitle
} from "@mui/material";
-import {useEffect, useState} from "react";
+import {useCallback, useEffect, useState} from "react";
import TextField from "@mui/material/TextField";
import dataModel from "../../../lib/dataModel";
import {useNavigate} from "react-router-dom";
import {enqueueSnackbar} from "notistack";
-export default function ContainerDialogCommit({open, container, onClose}) {
+export default function ContainerDialogCommit({open, containerName, containerIdShort, onClose}) {
const navigate = useNavigate();
const [actioning, setActioning] = useState(false);
const [tag, setTag] = useState('');
const [submitTimes, setSubmitTimes] = useState(0);
- const handleDialogClose = () => {
+ const handleDialogClose = useCallback(() => {
+ setTag('');
onClose();
- };
+ }, [onClose]);
const handleDialogForceClose = () => {
if (actioning) {
@@ -44,14 +45,14 @@ export default function ContainerDialogCommit({open, container, onClose}) {
}
const ac = new AbortController();
- dataModel.containerAction(container.Id.substring(0, 12), {
+ dataModel.containerAction(containerIdShort, {
action: 'commit',
repoTag: tag
}, ac)
.then(() => {
- onClose();
+ handleDialogClose();
const msg = (
- Container {container.Name || container.Names[0]} ({container.Id.substring(0, 12)}) has been committed to an image with tag {tag}.
+ Container {containerName} ({containerIdShort}) has been committed to image {tag}.
);
enqueueSnackbar(msg, {
variant: 'success'
@@ -83,7 +84,7 @@ export default function ContainerDialogCommit({open, container, onClose}) {
});
return () => ac.abort();
- }, [actioning, container, navigate, onClose, tag]);
+ }, [actioning, containerName, containerIdShort, handleDialogClose, navigate, tag]);
return (
<>
@@ -99,7 +100,7 @@ export default function ContainerDialogCommit({open, container, onClose}) {
- Commit the container {container.Name || container.Names[0]} ({container.Id.substring(0, 12)}) to an image.
+ Commit the container {containerName} ({containerIdShort}) to an image.
- Do you really want to remove {container.Name || container.Names[0]} ({container.Id.substring(0, 12)})?
+ Do you really want to remove {containerName} ({containerIdShort})?
This container will be removed permanently. You cannot undo this action.
diff --git a/src/routes/Containers/List/ContainersList.js b/src/routes/Containers/List/ContainersList.js
index 9fe4e12..b9299f3 100644
--- a/src/routes/Containers/List/ContainersList.js
+++ b/src/routes/Containers/List/ContainersList.js
@@ -7,7 +7,9 @@ import {Tooltip} from "@mui/material";
import {getController} from "../../../lib/HostGuestController";
import IconButton from "@mui/material/IconButton";
import {Link as RouterLink} from 'react-router-dom';
-import {aioProvider} from "../../../lib/dataProvidor";
+import {aioProvider, isDisconnectError} from "../../../lib/dataProvidor";
+import {showWebsocketDisconnectError} from "../../../components/WebsocketDisconnectError";
+import ContainerUpLearnMore from "../../../components/ContainerUpLearnMore";
export default function ContainersList() {
const [loading, setLoading] = useState(true)
@@ -32,13 +34,21 @@ export default function ContainersList() {
if (error.response) {
e = error.response.data;
}
- setErrMsg(e);
- setLoading(false);
+ if (loading) {
+ setErrMsg(e);
+ setLoading(false);
+ } else {
+ if (isDisconnectError(error)) {
+ showWebsocketDisconnectError();
+ } else {
+ setErrMsg(e);
+ }
+ }
};
const cancel = aioProvider().containersList(onData, onError);
return () => cancel();
- }, [navigate]);
+ }, [loading, navigate]);
const barButtons = useMemo(() => (
@@ -70,10 +80,15 @@ export default function ContainersList() {
}, []);
return (
-
+ <>
+
+
+
+ >
+
);
}
\ No newline at end of file
diff --git a/src/routes/Containers/Logs/ContainerLogsTerminal.js b/src/routes/Containers/Logs/ContainerLogsTerminal.js
index fe6fdc8..c232cc1 100644
--- a/src/routes/Containers/Logs/ContainerLogsTerminal.js
+++ b/src/routes/Containers/Logs/ContainerLogsTerminal.js
@@ -1,21 +1,24 @@
-import MyTerminal from "../../../components/MyTerminal";
import {useCallback, useEffect} from "react";
-import Pipe from "../../../lib/Pipe";
import dataModel from "../../../lib/dataModel";
import {useNavigate} from "react-router-dom";
import {Box} from "@mui/material";
import term from "../../../lib/termUtil";
+import TwoWayPipe from "../../../lib/TwoWayPipe";
+import MyTerminalTwoWay from "../../../components/MyTerminalTwoWay";
export default function ContainerLogsTerminal({containerId, logOpts, wsTerminationWriter, stopActionOnReceive}) {
const navigate = useNavigate();
- const pipe = new Pipe();
- const writer = pipe.useWriter();
- const writerOnReceive = pipe.useOnReceive();
+ const dataPipe = new TwoWayPipe();
+ const leftSide = dataPipe.useLeft();
+ const leftWriter = leftSide.useWriter();
const connectLogs = useCallback((containerId, logOpts, writer) => {
- writer(term.reset + term.yellow('Connecting...'));
+ writer({
+ type: 'data',
+ data: term.reset + term.yellow('Connecting...')
+ });
const [promise, cancelFunc] = dataModel.containerLogs(containerId, logOpts);
@@ -28,12 +31,18 @@ export default function ContainerLogsTerminal({containerId, logOpts, wsTerminati
promise.then(handle => {
// console.log("Log open...")
- writer(term.reset);
+ writer({
+ type: 'data',
+ data: term.reset
+ });
handle.onReceive(d => {
// 0 (std out) + data...
// 1 (std err) + data...
- writer(d.substring(1).replaceAll('\n', '\r\n'));
+ writer({
+ type: 'data',
+ data: d.substring(1).replaceAll('\n', '\r\n')
+ });
});
handle.onClose(({code, reason}) => {
@@ -46,13 +55,19 @@ export default function ContainerLogsTerminal({containerId, logOpts, wsTerminati
const now = new Date().toLocaleString();
if (!terminated) {
- writer(term.crlf + term.red(`Session ended at ${now}.`));
+ writer({
+ type: 'data',
+ data: term.crlf + term.red(`Session ended at ${now}.`)
+ });
if (code !== 1000) {
let reasonStr = '';
if (reason) {
reasonStr = `: ${reason}`;
}
- writer(term.red(` (Abnormally${reasonStr})`));
+ writer({
+ type: 'data',
+ data: term.red(` (Abnormally${reasonStr})`)
+ });
}
}
@@ -68,7 +83,10 @@ export default function ContainerLogsTerminal({containerId, logOpts, wsTerminati
return;
}
const e = error.toString();
- writer(term.reset + term.red(`Cannot load logs: ${e}.`));
+ writer({
+ type: 'data',
+ data: term.reset + term.red(`Cannot load logs: ${e}.`)
+ });
if (wsTerminationWriter) {
wsTerminationWriter();
}
@@ -80,7 +98,10 @@ export default function ContainerLogsTerminal({containerId, logOpts, wsTerminati
terminated = true;
terminalCloser();
const now = new Date().toLocaleString();
- writer(term.crlf + term.red(`Session ended by user at ${now}.`));
+ writer({
+ type: 'data',
+ data: term.crlf + term.red(`Session ended by user at ${now}.`)
+ });
});
}
@@ -90,13 +111,13 @@ export default function ContainerLogsTerminal({containerId, logOpts, wsTerminati
useEffect(() => {
- const closer = connectLogs(containerId, logOpts, writer);
+ const closer = connectLogs(containerId, logOpts, leftWriter);
return () => closer();
- }, [connectLogs, logOpts, containerId, writer]);
+ }, [connectLogs, logOpts, containerId, leftWriter]);
return (
-
+
);
}
\ No newline at end of file
diff --git a/src/routes/Images/List/ImageActions.js b/src/routes/Images/List/ImageActions.js
index d893b1d..a57bc78 100644
--- a/src/routes/Images/List/ImageActions.js
+++ b/src/routes/Images/List/ImageActions.js
@@ -3,7 +3,7 @@ import IconButton from "@mui/material/IconButton";
import {green, orange} from "@mui/material/colors";
import {Link as RouterLink} from "react-router-dom";
import DeleteIcon from "@mui/icons-material/Delete";
-import {useState} from "react";
+import {useCallback, useState} from "react";
import AddCircleIcon from "@mui/icons-material/AddCircle";
import LocalOfferIcon from '@mui/icons-material/LocalOffer';
import ImageDialogRemove from "./ImageDialogRemove";
@@ -18,6 +18,10 @@ export default function ImageActions({image}) {
const canRemove = image.Containers === 0 || (image.RepoTags && image.RepoTags.length > 1);
+ const handleDialogClose = useCallback(() => {
+ setDialogTag(false);
+ }, []);
+
return (
<>
setDialogTag(false)}
+ imageIdShort={image.idShort}
+ onClose={handleDialogClose}
/>
diff --git a/src/routes/Images/List/ImageDialogTag.js b/src/routes/Images/List/ImageDialogTag.js
index c27a73b..b977ffb 100644
--- a/src/routes/Images/List/ImageDialogTag.js
+++ b/src/routes/Images/List/ImageDialogTag.js
@@ -7,21 +7,21 @@ import {
DialogTitle
} from "@mui/material";
import TextField from "@mui/material/TextField";
-import {useEffect, useState} from "react";
+import {useCallback, useEffect, useState} from "react";
import dataModel from "../../../lib/dataModel";
import {useNavigate} from "react-router-dom";
import {enqueueSnackbar} from "notistack";
-export default function ImageDialogTag({open, image, onClose}) {
+export default function ImageDialogTag({open, imageIdShort, onClose}) {
const navigate = useNavigate();
const [tag, setTag] = useState('');
const [submitTimes, setSubmitTimes] = useState(0);
const [actioning, setActioning] = useState(false);
- const handleDialogClose = () => {
+ const handleDialogClose = useCallback(() => {
onClose();
setTag('');
- }
+ }, [onClose]);
const handleDialogForceClose = () => {
if (actioning) {
@@ -45,17 +45,16 @@ export default function ImageDialogTag({open, image, onClose}) {
}
const ac = new AbortController();
- dataModel.imageAction(image.idShort, {
+ dataModel.imageAction(imageIdShort, {
action: 'tag',
repoTag: tag
}, ac)
.then(() => {
- onClose();
+ handleDialogClose();
const msg = (
- Tag {tag} added to {image.idShort}.
+ Tag {tag} added to {imageIdShort}.
);
enqueueSnackbar(msg, {variant: 'success'});
- setTag('');
})
.catch(err => {
if (ac.signal.aborted) {
@@ -81,7 +80,7 @@ export default function ImageDialogTag({open, image, onClose}) {
});
return () => ac.abort();
- }, [actioning, image, navigate, onClose, tag]);
+ }, [actioning, handleDialogClose, imageIdShort, navigate, tag]);
return (