Skip to content

Commit

Permalink
Merge pull request #163 from lsst-sqre/tickets/DM-43461
Browse files Browse the repository at this point in the history
DM-43461: Enable background page recomputation in Times Square
  • Loading branch information
jonathansick authored Apr 11, 2024
2 parents 326ac7f + 72dd989 commit 482ee35
Show file tree
Hide file tree
Showing 22 changed files with 537 additions and 229 deletions.
7 changes: 7 additions & 0 deletions .changeset/brown-dingos-mix.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/chatty-bikes-unite.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/seven-maps-drum.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions apps/squareone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions apps/squareone/src/components/Button/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
`;
21 changes: 19 additions & 2 deletions apps/squareone/src/components/TimesSquareApp/TimesSquareApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ? (
<TimesSquarePrGitHubNav owner={owner} repo={repo} commitSha={commit} />
) : (
<TimesSquareMainGitHubNav pagePath={githubSlug} />
);

return (
<StyledLayout>
<Sidebar pageNav={pageNav} pagePanel={pagePanel} />
<Sidebar
pageNav={pageNav}
pagePanel={tsSlug ? <TimesSquareGitHubPagePanel /> : null}
/>
<main>{children}</main>
</StyledLayout>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<StyledContainer>
<StyledContent>
Computed{' '}
<time
dateTime={htmlEvent.dateFinished}
title={htmlEvent.dateFinished}
>
{formatDistanceToNow(dateFinished, { addSuffix: true })}
</time>{' '}
in {formattedDuration} seconds.
</StyledContent>
<GhostButton onClick={handleRecompute}>Recompute</GhostButton>
</StyledContainer>
);
}

if (htmlEvent.executionStatus == 'in_progress') {
return (
<StyledContainer>
<p>Computing…</p>
</StyledContainer>
);
}

return null;
}

const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 2rem;
`;

const StyledContent = styled.p`
font-size: 0.8rem;
margin-bottom: 0;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';

import { TimesSquareHtmlEventsContext } from '../TimesSquareHtmlEventsProvider';
import ExecStats from './ExecStats';

export default {
component: ExecStats,
title: 'Components/TimesSquare/ExecStats',
parameters: {
viewport: {
viewports: {
sidebar: {
name: 'Sidebar',
styles: {
width: '280px',
height: '900px',
},
},
},
},
defaultViewport: 'sidebar',
},
};

const Template = (args) => (
<TimesSquareHtmlEventsContext.Provider value={args}>
<ExecStats />
</TimesSquareHtmlEventsContext.Provider>
);

export const Default = Template.bind({});
Default.args = {
dateSubmitted: '2021-09-01T12:00:00Z',
dateStarted: '2021-09-01T12:00:01Z',
dateFinished: '2021-09-01T12:00:10Z',
executionStatus: 'complete',
executionDuration: 10.12,
};

export const InProgressNew = Template.bind({});
InProgressNew.args = {
dateSubmitted: '2021-09-01T12:00:10Z',
dateStarted: null,
dateFinished: null,
executionStatus: 'in_progress',
executionDuration: null,
};

export const InProgressExisting = Template.bind({});
InProgressExisting.args = {
dateSubmitted: '2021-09-01T12:00:00Z',
dateStarted: '2021-09-01T12:00:01Z',
dateFinished: '2021-09-01T12:00:10Z',
executionStatus: 'in_progress',
executionDuration: 10.12,
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@
* the notebook content (NotebookIframe).
*/

import React from 'react';
import styled from 'styled-components';
import getConfig from 'next/config';
import Head from 'next/head';
import Error from 'next/error';

import useTimesSquarePage from '../../hooks/useTimesSquarePage';
import TimesSquareParameters from '../TimesSquareParameters';
import ExecStats from './ExecStats';

export default function TimesSquareGitHubPagePanel({
tsPageUrl,
userParameters,
displaySettings,
}) {
export default function TimesSquareGitHubPagePanel({}) {
const { publicRuntimeConfig } = getConfig();
const pageData = useTimesSquarePage(tsPageUrl);
const pageData = useTimesSquarePage();

if (pageData.loading) {
return <p>Loading...</p>;
Expand All @@ -39,11 +37,9 @@ export default function TimesSquareGitHubPagePanel({
{description && (
<div dangerouslySetInnerHTML={{ __html: description.html }}></div>
)}
<TimesSquareParameters
pageData={pageData}
userParameters={userParameters}
displaySettings={displaySettings}
/>
<TimesSquareParameters />

<ExecStats />
</div>
</PagePanelContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Context provider for the Times Square /pages/:page/html/events endpoint.
*/

import React from 'react';
import { fetchEventSource } from '@microsoft/fetch-event-source';

import useTimesSquarePage from '../../hooks/useTimesSquarePage';
import { TimesSquareUrlParametersContext } from '../TimesSquareUrlParametersProvider';

export const TimesSquareHtmlEventsContext = React.createContext();

export default function TimesSquareHtmlEventsProvider({ children }) {
const [htmlEvent, setHtmlEvent] = React.useState(null);

const urlParameters = React.useContext(TimesSquareUrlParametersContext);
const timesSquarePage = useTimesSquarePage();

const urlQueryString = urlParameters.urlQueryString;
const htmlEventsUrl = timesSquarePage.htmlEventsUrl;
const fullHtmlEventsUrl = htmlEventsUrl
? `${htmlEventsUrl}?${urlQueryString}`
: null;

React.useEffect(() => {
const abortController = new AbortController();

async function runEffect() {
if (htmlEventsUrl) {
await fetchEventSource(fullHtmlEventsUrl, {
method: 'GET',
signal: abortController.signal,
onopen(res) {
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
console.log(`Client side error ${fullHtmlEventsUrl}`, res);
}
},
onmessage(event) {
const parsedData = JSON.parse(event.data);
setHtmlEvent(parsedData);
},
onclose() {},
onerror(err) {},
});
}
}
runEffect();

return () => {
// Clean up: close the event source connection
abortController.abort();
};
}, [fullHtmlEventsUrl, htmlEventsUrl]);

const contextValue = {
dateSubmitted: htmlEvent ? htmlEvent.date_submitted : null,
dateStarted: htmlEvent ? htmlEvent.date_started : null,
dateFinished: htmlEvent ? htmlEvent.date_finished : null,
executionStatus: htmlEvent ? htmlEvent.execution_status : null,
executionDuration: htmlEvent ? htmlEvent.execution_duration : null,
htmlHash: htmlEvent ? htmlEvent.html_hash : null,
htmlUrl: htmlEvent ? htmlEvent.html_url : null,
};

return (
<TimesSquareHtmlEventsContext.Provider value={{ ...contextValue }}>
{children}
</TimesSquareHtmlEventsContext.Provider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './TimesSquareHtmlEventsProvider';
export { default } from './TimesSquareHtmlEventsProvider';
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
* from Times Square with a notebook render.
*/

import React from 'react';
import styled from 'styled-components';

import useHtmlStatus from './useHtmlStatus';
import { TimesSquareUrlParametersContext } from '../TimesSquareUrlParametersProvider';

const StyledIframe = styled.iframe`
/* --shadow-color: 0deg 0% 74%;
Expand All @@ -19,12 +21,9 @@ const StyledIframe = styled.iframe`
height: 100%;
`;

export default function TimesSquareNotebookViewer({
tsPageUrl,
parameters,
displaySettings,
}) {
const htmlStatus = useHtmlStatus(tsPageUrl, parameters, displaySettings);
export default function TimesSquareNotebookViewer({}) {
const { tsPageUrl } = React.useContext(TimesSquareUrlParametersContext);
const htmlStatus = useHtmlStatus();

if (htmlStatus.error) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
* dynamic refreshing of data about a page's HTML rendering.
*/

import React from 'react';
import useSWR from 'swr';
import useTimesSquarePage from '../../hooks/useTimesSquarePage';
import { TimesSquareUrlParametersContext } from '../TimesSquareUrlParametersProvider';

const fetcher = (...args) => fetch(...args).then((res) => res.json());

Expand All @@ -20,8 +22,11 @@ export function parameterizeUrl(baseUrl, parameters, displaySettings) {
return url.toString();
}

function useHtmlStatus(pageUrl, parameters, displaySettings) {
const pageData = useTimesSquarePage(pageUrl);
function useHtmlStatus() {
const { notebookParameters: parameters, displaySettings } = React.useContext(
TimesSquareUrlParametersContext
);
const pageData = useTimesSquarePage();

const { data, error } = useSWR(
() => parameterizeUrl(pageData.htmlStatusUrl, parameters, displaySettings),
Expand Down
Loading

0 comments on commit 482ee35

Please sign in to comment.