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 => ( + + ); + + 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 ( - Add a tag to the image {image.idShort} + Add a tag to the image {imageIdShort} cancel(); - }, [navigate]); + }, [loading, navigate]); useEffect(() => { const ctrl = getController('bar_button'); @@ -58,17 +68,25 @@ export default function ImageList() { }, []); return ( - + <> + + + + ); } export function ImageListBarButtons() { const [pullDialogOpen, setPullDialogOpen] = useState(false); + const handleCloseDialog = useCallback(() => { + setPullDialogOpen(false); + }, []); + return ( <> @@ -81,7 +99,7 @@ export function ImageListBarButtons() { - setPullDialogOpen(false)} /> + ); } diff --git a/src/routes/Images/List/ImagePull.js b/src/routes/Images/List/ImagePullDialog.js similarity index 83% rename from src/routes/Images/List/ImagePull.js rename to src/routes/Images/List/ImagePullDialog.js index cff5f9a..7d89230 100644 --- a/src/routes/Images/List/ImagePull.js +++ b/src/routes/Images/List/ImagePullDialog.js @@ -1,11 +1,13 @@ -import {Box, Dialog, DialogContent, DialogContentText, DialogTitle} from "@mui/material"; +import {Alert, Box, Dialog, DialogContent, DialogContentText, DialogTitle} from "@mui/material"; import TextField from "@mui/material/TextField"; import {useRef, useState} from "react"; import ImagePullTerminal from "./ImagePullTerminal"; import ImagePullActions from "./ImagePullActions"; import Pipe from "../../../lib/Pipe"; -export default function ImagePull({dialogOpen, onClose}) { +export const demoImage = 'docker.io/library/ubuntu:latest'; + +export default function ImagePullDialog({open, onClose}) { const [imageName, setImageName] = useState(''); const [pulling, setPulling] = useState(false); const done = useRef(false); @@ -38,10 +40,14 @@ export default function ImagePull({dialogOpen, onClose}) { pullTerminationWriter(); }; + const handleClickDemoImage = () => { + setImageName(demoImage); + }; + return ( <> + + {process.env.REACT_APP_CONTAINERUP_DEMO && ( + + As a limit of the demo server, please try this one: {demoImage} + + )} ) : ( <> diff --git a/src/routes/Login.js b/src/routes/Login.js index 2e1e89d..fb0ada5 100644 --- a/src/routes/Login.js +++ b/src/routes/Login.js @@ -7,10 +7,23 @@ import {useState} from "react"; import dataModel from '../lib/dataModel'; import {Container, Snackbar, Alert} from "@mui/material"; import {useNavigate, useSearchParams} from "react-router-dom"; +import {AlertTitle} from "@mui/lab"; +import {grey} from "@mui/material/colors"; +import ContainerUpLearnMore from "../components/ContainerUpLearnMore"; +import {useGA4} from "../lib/ga4"; + +let defaultUsername = ''; +let defaultPassword = ''; +if (process.env.REACT_APP_CONTAINERUP_DEMO) { + defaultUsername = 'demo'; + defaultPassword = 'demo'; +} export default function Login() { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); + useGA4(); + + const [username, setUsername] = useState(defaultUsername); + const [password, setPassword] = useState(defaultPassword); const [loading, setLoading] = useState(false); const [showAlert, setShowAlert] = useState(false); const [errMsg, setErrMsg] = useState('') @@ -97,6 +110,21 @@ export default function Login() { Submit + + {process.env.REACT_APP_CONTAINERUP_DEMO && ( + <> + + Demo server + Feel free to explore all the features! + + + + + )} + + + Build {process.env.REACT_APP_CONTAINERUP_BUILD} Commit {process.env.REACT_APP_CONTAINERUP_COMMIT} + diff --git a/src/routes/Root.js b/src/routes/Root.js index 2c15f71..4ecbbc9 100644 --- a/src/routes/Root.js +++ b/src/routes/Root.js @@ -14,8 +14,10 @@ import {Box} from "@mui/material"; import AppBarButtons from "./AppBarButtons"; import AppBarBreadcrumb from "./AppBarBreadcrumb"; import {SnackbarProvider} from "notistack"; +import {useGA4} from "../lib/ga4"; export default function Root() { + useGA4(); const [drawerOpen, setDrawerOpen] = useState(false); const handleDrawerOpen = () => { @@ -69,7 +71,7 @@ export default function Root() { path='/info' /> } path='/logout'