diff --git a/CHANGELOG.md b/CHANGELOG.md index d82bddb6f..d6ae39665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # AmpliPi Software Releases # Future Release +* Web App + * Add warning for older versions of the webapp running on newer backends # 0.4.11 @@ -9,7 +11,6 @@ * Web App * Changed caching rules to ensure that users don't get stuck with old versions of the webapp post update - ## 0.4.10 * Web App diff --git a/NEW_RELEASE.md b/NEW_RELEASE.md index 2365ae2ce..45fa84ed5 100644 --- a/NEW_RELEASE.md +++ b/NEW_RELEASE.md @@ -31,7 +31,8 @@ This project follows [Semantic Versioning](https://semver.org/). Here are some e - [ ] Update the API by running `scripts/create_spec` script. - [ ] Create & merge a branch/PR off `main` to bump the version in the CHANGELOG and also using `poetry version ${VERSION}` - [ ] Checkout main & create a detached HEAD: `git checkout main; git pull; git checkout --detach` -- [ ] Build the webapp in `web` with `npm run build` and force add the changes with `git add -f web/dist; git commit -m "Build web app for release"` +- [ ] CD into `web`, Run `echo "VITE_BACKEND_VERSION=$(poetry version -s)" > .env` +- [ ] Still in `web`, build the webapp with `npm run build` and force add the changes with `git add -f web/dist; git commit -m "Build web app for release"` - [ ] Tag the changes so we can make a release on GitHub: `git tag -as ${VERSION} -m '' && git push origin ${VERSION}` - [ ] Make a release using the GitHub interface - [ ] Use the AmpliPi updater to update to the release diff --git a/scripts/deploy b/scripts/deploy index 4361d20aa..5d60fbefb 100755 --- a/scripts/deploy +++ b/scripts/deploy @@ -159,6 +159,7 @@ poetry version ${git_info} echo "Building web app" pushd ../web # Change to web directory npm install # Install nodejs dependencies +echo "VITE_BACKEND_VERSION=$(poetry version -s)" > .env # Collect the version number and bake it into the frontend npm run build # Build the web app popd diff --git a/web/src/App.jsx b/web/src/App.jsx index 4e28a7d3c..229f8988e 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -13,6 +13,7 @@ import DisconnectedIcon from "./components/DisconnectedIcon/DisconnectedIcon"; import Browse from "@/pages/Browse/Browse"; import PropTypes from "prop-types"; +import AlertBar from "./components/StatusBars/AlertBar"; // holds onto the selectedSource state so that it persists between refreshes export const usePersistentStore = create( @@ -40,6 +41,7 @@ export const useStatusStore = create((set, get) => ({ skipUpdate: false, loaded: false, // using this instead of (status === null) because it fixes the re-rendering issue disconnected: true, + alert: {"open": false, "text": "", "onClose": () => {}}, skipNextUpdate: () => { set({ skipUpdate: true }); }, @@ -122,6 +124,9 @@ export const useStatusStore = create((set, get) => ({ } }); }, + setAlert: (text, onClose) => { + set({alert: {"open": true, "text": text, "onClose": onClose()}}); + }, getSystemState: () => { fetch("/api") @@ -133,6 +138,9 @@ export const useStatusStore = create((set, get) => ({ set({ skipUpdate: false }); } else { set({ status: s, loaded: true, disconnected: false }); + if(s.info.version != import.meta.env.VITE_BACKEND_VERSION){ + set({alert: {"open": true, "text": "Your webapp is out of date, closing this message will refresh the page. If this message persists post-refresh, clear your browser cache and try again.", "onClose": () => {window.location.reload();}}}); + } } }); } else if (res.status == 401) { @@ -225,15 +233,19 @@ Page.propTypes = { }; const App = ({ selectedPage }) => { + const alert = useStatusStore((s) => s.alert); return ( -
+
-
{/* Used to make sure the background doesn't stretch or stop prematurely on scrollable pages */} -
- -
- +
{/* Used to make sure the background doesn't stretch or stop prematurely on scrollable pages */}
+
+ {alert["open"] == false; alert["onClose"]();}}/> +
+
+
+ +
); }; App.propTypes = { diff --git a/web/src/App.scss b/web/src/App.scss index 6a7bfb696..b99b6ca38 100644 --- a/web/src/App.scss +++ b/web/src/App.scss @@ -20,6 +20,7 @@ } // unused + .app-container { display: flex; width: 100%; @@ -31,10 +32,25 @@ padding-bottom: general.$navbar-height; } + +.alert{ + position: fixed; + top: 10px; + left: 10px; + right: 10px; + z-index: 2; +} + @media (general.$is-landscape){ - // Themed scrollbar is overridden by 90% of mobile apps and browsers, but only partially overridden by one (andorid app) + // Themed scrollbar is overridden by 90% of mobile apps and browsers, but only partially overridden by one (android app) // This still looks better on desktop, so gate it to only being on desktop without running the risk for the weird android app situation + .alert{ + left: 25vw; + right: 25vw; + top: 10px; + } + .pill-scrollbar { max-height: inherit; overflow-y: auto; diff --git a/web/src/components/CreateStreamModal/StreamModal/StreamModal.jsx b/web/src/components/CreateStreamModal/StreamModal/StreamModal.jsx index 2d27fbb8f..685131d22 100644 --- a/web/src/components/CreateStreamModal/StreamModal/StreamModal.jsx +++ b/web/src/components/CreateStreamModal/StreamModal/StreamModal.jsx @@ -251,7 +251,6 @@ const StreamModal = ({ stream, onClose, apply, del }) => { renderAnimationState={renderAlertAnimation} open={hasError} onClose={() => {setHasError(false);}} - status={false} text={errorMessage} /> } diff --git a/web/src/components/StatusBars/AlertBar.jsx b/web/src/components/StatusBars/AlertBar.jsx index 8d5aaa682..2ae5e6706 100644 --- a/web/src/components/StatusBars/AlertBar.jsx +++ b/web/src/components/StatusBars/AlertBar.jsx @@ -7,7 +7,7 @@ import Alert from "@mui/material/Alert"; export default function AlertBar(props) { const { open, - status, + success, text, onClose, renderAnimationState, @@ -19,34 +19,37 @@ export default function AlertBar(props) { if(alertRef.current != null){ const alertComp = alertRef.current; alertComp.classList.remove("error"); - if(status == false){ + if(!success){ alertComp.offsetWidth; alertComp.classList.add("error"); } } - }, [status, renderAnimationState]); + }, [success, renderAnimationState]); - if(open){ + const [closedText, setClosedText] = React.useState(""); // If a user has closed a given message, don't show it again until another message tries to appear + + if(open && text != closedText){ return( {onClose(); setClosedText(text);}} + severity={success ? "success" : "error"} variant="filled" style={{width: "100%",}} > {text} - ) + ); } -}; +} AlertBar.propTypes = { open: PropTypes.bool.isRequired, - status: PropTypes.bool.isRequired, + success: PropTypes.bool, text: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, renderAnimationState: PropTypes.number, }; AlertBar.defaultProps = { + success: false, renderAnimationState: 1, -} +}; diff --git a/web/src/components/StatusBars/ResponseBar.jsx b/web/src/components/StatusBars/ResponseBar.jsx index 337bfac29..d4c94f8a2 100644 --- a/web/src/components/StatusBars/ResponseBar.jsx +++ b/web/src/components/StatusBars/ResponseBar.jsx @@ -41,7 +41,7 @@ export default function ResponseBar(props) { return( {setOpen(false);}} /> diff --git a/web/src/components/StatusBars/StatusBar.jsx b/web/src/components/StatusBars/StatusBar.jsx index 0f20c8f76..c54326513 100644 --- a/web/src/components/StatusBars/StatusBar.jsx +++ b/web/src/components/StatusBars/StatusBar.jsx @@ -3,38 +3,50 @@ import PropTypes from "prop-types"; import "./StatusBars.scss"; import Snackbar from "@mui/material/Snackbar"; -import Alert from "@mui/material/Alert" +import Alert from "@mui/material/Alert"; export default function StatusBar(props) { const { open, - status, + success, text, onClose, + anchorOrigin, + autoHideDuration, } = props; + const [closedText, setClosedText] = React.useState(""); // If a user has closed a given message, don't show it again until another message tries to appear + return( {onClose(); setClosedText(text);}} > {onClose(); setClosedText(text);}} + severity={success ? "success" : "error"} variant="filled" style={{width: "100%"}} > {text} - ) -}; + ); +} StatusBar.propTypes = { open: PropTypes.bool.isRequired, - status: PropTypes.bool.isRequired, + success: PropTypes.bool, text: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, + autoHideDuration: PropTypes.number, + anchorOrigin: PropTypes.object, }; +StatusBar.defaultProps = { + success: false, + autoHideDuration: 3000, + anchorOrigin: {vertical: "bottom", horizontal: "left"}, +}; + diff --git a/web/src/pages/Browse/Browse.jsx b/web/src/pages/Browse/Browse.jsx index 3c8495175..cef246723 100644 --- a/web/src/pages/Browse/Browse.jsx +++ b/web/src/pages/Browse/Browse.jsx @@ -113,7 +113,6 @@ const Browse = () => { {setErrorOpen(false);}} /> diff --git a/web/src/pages/Home/Home.jsx b/web/src/pages/Home/Home.jsx index 0f28f48bc..6a91fd8ae 100644 --- a/web/src/pages/Home/Home.jsx +++ b/web/src/pages/Home/Home.jsx @@ -70,7 +70,7 @@ const Home = () => { const [showStatus, setShowStatus] = React.useState(false); const statusText = React.useRef(""); - const status = React.useRef(true); + const success = React.useRef(true); let nextAvailableSource = null; let cards = []; @@ -145,10 +145,10 @@ const Home = () => { )} {presetsModalOpen && ( {status.current = state; statusText.current = text; setShowStatus(true);}} + onApply={(state, text) => {success.current = state; statusText.current = text; setShowStatus(true);}} onClose={() => setPresetsModalOpen(false)} /> )} - {setShowStatus(false);}} /> + {setShowStatus(false);}} />
); };