diff --git a/.changeset/brown-dingos-mix.md b/.changeset/brown-dingos-mix.md
new file mode 100644
index 00000000..14101684
--- /dev/null
+++ b/.changeset/brown-dingos-mix.md
@@ -0,0 +1,7 @@
+---
+'squareone': minor
+---
+
+Implement background recomputation for cached Times Square pages. The "Recompute" button submits a request to Times Square's `DELETE /v1/pages/:page/html?{params}` endpoint, which causes a background recomputation of the notebook and re-rendering of the cached HTML.
+
+The new `TimesSquareHtmlEventsProvider` is a React context provider that provides real-time updates from Times Square about the status of an HTML rendering for a given set of parameters using Times Square's `/v1/pages/:page/html/events/{params}` endpoint. Squareone uses `@microsoft/fetch-event-source` to subscribe to this server-sent events (SSE) endpoint. Using this provider, the UI is able to show new data to the user, including the status of the computation, and once the computation is complete, the date/age of computation and the execution time.
diff --git a/.changeset/chatty-bikes-unite.md b/.changeset/chatty-bikes-unite.md
new file mode 100644
index 00000000..070246d0
--- /dev/null
+++ b/.changeset/chatty-bikes-unite.md
@@ -0,0 +1,5 @@
+---
+'squareone': minor
+---
+
+The Times Square "Update" and "Reset" buttons are now disabled when appropriate. The Update button is disabled when the parameter inputs have not been changed relative to their current state. Likewise, the Reset button is disabled when the parameters are unchanged from the current state.
diff --git a/.changeset/seven-maps-drum.md b/.changeset/seven-maps-drum.md
new file mode 100644
index 00000000..3b0e3cbb
--- /dev/null
+++ b/.changeset/seven-maps-drum.md
@@ -0,0 +1,5 @@
+---
+'squareone': minor
+---
+
+New `TimesSquareUrlParametersProvider` component. This React context provides the URL-based state to Times Square components, such as the page being viewed, its notebook parameters values, and the display settings. This change simplifies the structure of the React pages by refactoring all of the URL parsing into a common component. As well, this context eliminates "prop drilling" to provide this URL-based state to all components in the Times Square application.
diff --git a/apps/squareone/package.json b/apps/squareone/package.json
index bef3bffd..822d86d5 100644
--- a/apps/squareone/package.json
+++ b/apps/squareone/package.json
@@ -42,9 +42,11 @@
"@lsst-sqre/global-css": "workspace:*",
"@lsst-sqre/rubin-style-dictionary": "workspace:*",
"@lsst-sqre/squared": "workspace:*",
+ "@microsoft/fetch-event-source": "^2.0.1",
"@reach/alert": "^0.17.0",
"@reach/menu-button": "^0.17.0",
"ajv": "^8.11.0",
+ "date-fns": "^3.6.0",
"formik": "^2.2.9",
"js-yaml": "^4.1.0",
"next": "^12.2.4",
diff --git a/apps/squareone/src/components/Button/Button.js b/apps/squareone/src/components/Button/Button.js
index c780bc79..5f079fd8 100644
--- a/apps/squareone/src/components/Button/Button.js
+++ b/apps/squareone/src/components/Button/Button.js
@@ -16,6 +16,9 @@ const Button = styled.button`
&:active {
background-color: var(--sqo-primary-button-background-color-active);
}
+ &:disabled {
+ cursor: not-allowed;
+ }
`;
export default Button;
@@ -36,4 +39,12 @@ export const GhostButton = styled.button`
export const RedGhostButton = styled(GhostButton)`
--ghost-button-color: var(--rsd-color-red-500);
+
+ &:disabled {
+ // instead of setting opacity it'd be better to have a lighter red color
+ opacity: 0.5;
+ cursor: not-allowed;
+ background-color: transparent;
+ color: #000;
+ }
`;
diff --git a/apps/squareone/src/components/TimesSquareApp/TimesSquareApp.js b/apps/squareone/src/components/TimesSquareApp/TimesSquareApp.js
index 983d195c..24cbdf2e 100644
--- a/apps/squareone/src/components/TimesSquareApp/TimesSquareApp.js
+++ b/apps/squareone/src/components/TimesSquareApp/TimesSquareApp.js
@@ -4,9 +4,14 @@
* page, usually the page viewer, or a markdown view of the GitHub repository.
*/
+import React from 'react';
import styled from 'styled-components';
import Sidebar from './Sidebar';
+import { TimesSquareUrlParametersContext } from '../TimesSquareUrlParametersProvider';
+import TimesSquareMainGitHubNav from '../TimesSquareMainGitHubNav';
+import TimesSquarePrGitHubNav from '../TimesSquarePrGitHubNav';
+import TimesSquareGitHubPagePanel from '../TimesSquareGitHubPagePanel/TimesSquareGitHubPagePanel';
const StyledLayout = styled.div`
display: flex;
@@ -20,10 +25,22 @@ const StyledLayout = styled.div`
}
`;
-export default function TimesSquareApp({ children, pageNav, pagePanel }) {
+export default function TimesSquareApp({ children }) {
+ const { tsSlug, owner, repo, commit, githubSlug, urlQueryString } =
+ React.useContext(TimesSquareUrlParametersContext);
+
+ const pageNav = commit ? (
+
+ ) : (
+
+ );
+
return (
-
+ : null}
+ />
{children}
);
diff --git a/apps/squareone/src/components/TimesSquareGitHubPagePanel/ExecStats.js b/apps/squareone/src/components/TimesSquareGitHubPagePanel/ExecStats.js
new file mode 100644
index 00000000..42d570bd
--- /dev/null
+++ b/apps/squareone/src/components/TimesSquareGitHubPagePanel/ExecStats.js
@@ -0,0 +1,69 @@
+/*
+ * ExecStats provides a summary of the execution status and timing of the
+ * notebook execution. It also provides a button to request the recomputation
+ * of the already-executed notebook.
+ */
+
+import React from 'react';
+import styled from 'styled-components';
+import { parseISO, formatDistanceToNow } from 'date-fns';
+
+import { TimesSquareHtmlEventsContext } from '../TimesSquareHtmlEventsProvider';
+import { GhostButton } from '../Button';
+
+export default function ExecStats({}) {
+ const htmlEvent = React.useContext(TimesSquareHtmlEventsContext);
+
+ const handleRecompute = async (event) => {
+ event.preventDefault();
+
+ await fetch(htmlEvent.htmlUrl, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ };
+
+ if (htmlEvent.executionStatus == 'complete') {
+ const dateFinished = parseISO(htmlEvent.dateFinished);
+ const formattedDuration = Number(htmlEvent.executionDuration).toFixed(1);
+ return (
+
+
+ Computed{' '}
+ {' '}
+ in {formattedDuration} seconds.
+
+ Recompute
+
+ );
+ }
+
+ if (htmlEvent.executionStatus == 'in_progress') {
+ return (
+
+