From 4006d4be0c36bdd78d8988be0d8f16849876e0a7 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Thu, 31 Mar 2022 17:24:29 -0400 Subject: [PATCH 1/6] Use setup-node@v3 with built-in caching --- .github/workflows/ci.yaml | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7a3a441..f101a8c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,16 +20,17 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Read .nvmrc id: node_version run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc) - name: Set up node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ steps.node_version.outputs.NODE_VERSION }} + cache: 'npm' - name: Authenticate GitHub Packages run: | @@ -37,17 +38,6 @@ jobs: env: NPM_PKG_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Cache downloaded Node.js modules - uses: actions/cache@v2 - with: - # npm cache files are stored in `~/.npm` on Linux/macOS - # Caching node_modules itself isn't compatible with npm ci - path: ~/.npm - key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.OS }}-node- - ${{ runner.OS }}- - - name: Install npm packages run: npm ci @@ -74,7 +64,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Define the Docker tag id: vars From 14fd4f515e75c04cce761d85a849dc5624874119 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Thu, 31 Mar 2022 17:32:32 -0400 Subject: [PATCH 2/6] Add a dependabot configuration This will keep both GitHub Actions and npm dependencies up to date. --- .github/dependabot.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4ddfea2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,30 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "thursday" + time: "09:00" + timezone: America/Toronto + reviewers: + - "jonathansick" + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + time: "09:00" + timezone: America/Toronto + # Generally this means we'll get minor and patch updates, but we'll + # manually need to roll out major version changes. + versioning-strategy: "lockfile-only" + registries: + - npm-github + reviewers: + - "jonathansick" +registries: + npm-github: + type: npm-registry + url: https://npm.pkg.github.com + # lsst-sqre org secret (for Dependabot) + token: ${{ secrets.READONLY_PACKAGES_GITHUB_TOKEN }} From 1a53ce5212b02b86c80ccf7e5a1f64f19d0ff6ef Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Fri, 11 Mar 2022 14:58:18 -0500 Subject: [PATCH 3/6] Refactor iframe into NotebookIframe component --- components/notebookIframe.js | 30 ++++++++++++++++++++++++++++++ pages/nb/[nbSlug].js | 6 +++++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 components/notebookIframe.js diff --git a/components/notebookIframe.js b/components/notebookIframe.js new file mode 100644 index 0000000..9dc722d --- /dev/null +++ b/components/notebookIframe.js @@ -0,0 +1,30 @@ +/* + * The NotebookIframe controls the iframe with HTML content + * from Times Square with a notebook render. + */ + +import styled from 'styled-components'; + +const StyledIframe = styled.iframe` + --shadow-color: 0deg 0% 74%; + --shadow-elevation-medium: 0.1px 0.7px 0.9px hsl(var(--shadow-color) / 0.16), + 0.4px 2.4px 3px -0.6px hsl(var(--shadow-color) / 0.2), + 0.8px 5.3px 6.7px -1.1px hsl(var(--shadow-color) / 0.24), + 1.9px 11.9px 15px -1.7px hsl(var(--shadow-color) / 0.28); + border: 0px solid black; + box-shadow: var(--shadow-elevation-medium); + width: 100%; + height: 100%; +`; + +export default function NotebookIframeContainer({ tsHtmlUrl, parameters }) { + // query string with parameters for requesting the corresponding + // notebook HTML render + const updatedQS = parameters + .map( + (item) => `${encodeURIComponent(item[0])}=${encodeURIComponent(item[1])}` + ) + .join('&'); + + return ; +} diff --git a/pages/nb/[nbSlug].js b/pages/nb/[nbSlug].js index 011abac..3086e98 100644 --- a/pages/nb/[nbSlug].js +++ b/pages/nb/[nbSlug].js @@ -4,6 +4,7 @@ import getConfig from 'next/config'; import { withRouter, useRouter } from '../../hooks/useRouter'; import { useFetch } from '../../hooks/fetch'; +import NotebookIframe from '../../components/notebookIframe'; const NotebookViewLayout = styled.div` display: flex; @@ -77,7 +78,10 @@ function TSNotebookViewer({ nbSlug, userParameters }) {

Status: {status}

- + ); From 964fbab645f6c2b17ecd691343e927f508f50181 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Fri, 11 Mar 2022 14:59:54 -0500 Subject: [PATCH 4/6] Add swr dependency https://swr.vercel.app/docs/getting-started swr provides smart data fetching hooks for nextjs/react. --- package-lock.json | 17 ++++++++++++++++- package.json | 3 ++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ebf93c6..7ec8b00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "normalize.css": "^8.0.1", "react": "^17.0.2", "react-dom": "^17.0.2", - "styled-components": "^5.3.3" + "styled-components": "^5.3.3", + "swr": "^1.2.2" }, "devDependencies": { "babel-plugin-styled-components": "^2.0.2", @@ -6010,6 +6011,14 @@ "node": ">=4" } }, + "node_modules/swr": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-1.2.2.tgz", + "integrity": "sha512-ky0BskS/V47GpW8d6RU7CPsr6J8cr7mQD6+do5eky3bM0IyJaoi3vO8UhvrzJaObuTlGhPl2szodeB2dUd76Xw==", + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -10846,6 +10855,12 @@ "has-flag": "^3.0.0" } }, + "swr": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-1.2.2.tgz", + "integrity": "sha512-ky0BskS/V47GpW8d6RU7CPsr6J8cr7mQD6+do5eky3bM0IyJaoi3vO8UhvrzJaObuTlGhPl2szodeB2dUd76Xw==", + "requires": {} + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index 4d965f1..576edae 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "normalize.css": "^8.0.1", "react": "^17.0.2", "react-dom": "^17.0.2", - "styled-components": "^5.3.3" + "styled-components": "^5.3.3", + "swr": "^1.2.2" }, "devDependencies": { "babel-plugin-styled-components": "^2.0.2", From dd542e7cc87a0297b8ce4f6f1b2bb7b352bbe179 Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Fri, 11 Mar 2022 17:57:49 -0500 Subject: [PATCH 5/6] Implement auto-refreshing notebook Times Square API provides a new /htmlstatus endpoint that provides metadata about the HTML render needed to support auto-refreshing the content: a flag of whether the HTML is available or not, and a digest of the HTML to indicate whether the client has the same version of the HTML as is on the server (i.e. to support notebooks with TTL settings). If the notebook is not available yet, this is set to a default value. Since swr revalidates data (every 1 second right now) the default key will be replaced with the digest. Changing this key on the iframe causes the iframe to reload. Note that useHtmlStatus is refactored into a reusable hook; other components can also use html status and share the same connection pool (so no additional requests are being made). --- components/notebookIframe.js | 42 +++++++++++++++---- hooks/htmlStatus.js | 30 +++++++++++++ next.config.js | 4 ++ pages/api/dev/times-square/v1/pages/[page].js | 1 + .../v1/pages/[page]/htmlstatus.js | 26 ++++++++++++ pages/nb/[nbSlug].js | 16 +++---- 6 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 hooks/htmlStatus.js create mode 100644 pages/api/dev/times-square/v1/pages/[page]/htmlstatus.js diff --git a/components/notebookIframe.js b/components/notebookIframe.js index 9dc722d..c7d9204 100644 --- a/components/notebookIframe.js +++ b/components/notebookIframe.js @@ -5,6 +5,8 @@ import styled from 'styled-components'; +import useHtmlStatus from '../hooks/htmlStatus'; + const StyledIframe = styled.iframe` --shadow-color: 0deg 0% 74%; --shadow-elevation-medium: 0.1px 0.7px 0.9px hsl(var(--shadow-color) / 0.16), @@ -17,14 +19,36 @@ const StyledIframe = styled.iframe` height: 100%; `; -export default function NotebookIframeContainer({ tsHtmlUrl, parameters }) { - // query string with parameters for requesting the corresponding - // notebook HTML render - const updatedQS = parameters - .map( - (item) => `${encodeURIComponent(item[0])}=${encodeURIComponent(item[1])}` - ) - .join('&'); +export default function NotebookIframe({ + tsHtmlUrl, + tsHtmlStatusUrl, + parameters, +}) { + const htmlUrl = new URL(tsHtmlUrl); + parameters.forEach((item) => htmlUrl.searchParams.set(item[0], item[1])); + + const htmlStatus = useHtmlStatus(tsHtmlStatusUrl, parameters); + + if (htmlStatus.error) { + return ( +
+

Error contacting API at {`${tsHtmlStatusUrl}`}

+
+ ); + } + + if (htmlStatus.loading) { + return ( +
+

Loading...

+
+ ); + } - return ; + return ( + + ); } diff --git a/hooks/htmlStatus.js b/hooks/htmlStatus.js new file mode 100644 index 0000000..6425316 --- /dev/null +++ b/hooks/htmlStatus.js @@ -0,0 +1,30 @@ +/* + * useHtmlStatus hook fetches data from the Times Square + * /v1/pages/:page/htmlstatus endpoint using the SWR hook to enable + * dynamic refreshing of data about a page's HTML rendering. + */ + +import useSWR from 'swr'; + +const fetcher = (...args) => fetch(...args).then((res) => res.json()); + +function useHtmlStatus(htmlStatusUrl, parameters) { + const url = new URL(htmlStatusUrl); + parameters.forEach((item) => url.searchParams.set(item[0], item[1])); + const fullHtmlStatusUrl = url.toString(); + + const { data, error } = useSWR(fullHtmlStatusUrl, fetcher, { + refreshInterval: 1000, // ping every 1 second while brower in focus + }); + + return { + error: error, + loading: !error && !data, + htmlAvailable: data ? data.available : false, + htmlHash: data ? data.html_hash : null, + htmlUrl: data ? data.html_url : null, + iframeKey: data && data.available ? data.html_hash : 'html-not-available', + }; +} + +export default useHtmlStatus; diff --git a/next.config.js b/next.config.js index 9fdc70f..0390fbf 100644 --- a/next.config.js +++ b/next.config.js @@ -61,6 +61,10 @@ module.exports = (phase, { defaultConfig }) => { source: '/api/v1/pages/:page/html', destination: '/api/dev/times-square/v1/pages/:page/html', }, + { + source: '/api/v1/pages/:page/htmlstatus', + destination: '/api/dev/times-square/v1/pages/:page/htmlstatus', + }, { source: '/api/v1/pages/:page', destination: '/api/dev/times-square/v1/pages/:page', diff --git a/pages/api/dev/times-square/v1/pages/[page].js b/pages/api/dev/times-square/v1/pages/[page].js index d6f8d37..4d647a0 100644 --- a/pages/api/dev/times-square/v1/pages/[page].js +++ b/pages/api/dev/times-square/v1/pages/[page].js @@ -22,6 +22,7 @@ export default function handler(req, res) { source_url: `${pageBaseUrl}/source`, rendered_url: `${pageBaseUrl}/rendered`, html_url: `${pageBaseUrl}/html`, + html_status_url: `${pageBaseUrl}/htmlstatus`, parameters: { a: { type: 'number', diff --git a/pages/api/dev/times-square/v1/pages/[page]/htmlstatus.js b/pages/api/dev/times-square/v1/pages/[page]/htmlstatus.js new file mode 100644 index 0000000..746268f --- /dev/null +++ b/pages/api/dev/times-square/v1/pages/[page]/htmlstatus.js @@ -0,0 +1,26 @@ +/* + * Mock Times Square API endpoint: /times-square/api/v1/pages/:page/htmlstatus + */ +import getConfig from 'next/config'; + +export default function handler(req, res) { + const { page, a } = req.query; + const { publicRuntimeConfig } = getConfig(); + const { timesSquareApiUrl } = publicRuntimeConfig; + + const pageBaseUrl = `${timesSquareApiUrl}/v1/pages/${page}`; + + const content = { + available: a != '2', // magic value to toggle status modes + html_url: `${pageBaseUrl}/html?a={a}`, + html_hash: a != '2' ? '12345' : null, + }; + + console.log(content); + + console.log('Pinged status'); + + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(content)); +} diff --git a/pages/nb/[nbSlug].js b/pages/nb/[nbSlug].js index 3086e98..36e1ace 100644 --- a/pages/nb/[nbSlug].js +++ b/pages/nb/[nbSlug].js @@ -44,7 +44,11 @@ function TSNotebookViewer({ nbSlug, userParameters }) { const { status, error, data } = useFetch(pageDataUrl); if (status === 'fetched') { - const { parameters, html_url: htmlApiUrl } = data; + const { + parameters, + html_url: htmlApiUrl, + html_status_url: htmlStatusApiUrl, + } = data; // Merge user-set parameters with defaults const updatedParameters = Object.entries(parameters).map((item) => { @@ -60,15 +64,6 @@ function TSNotebookViewer({ nbSlug, userParameters }) {
  • {`${item[0]}: ${item[1]}`}
  • )); - // query string with parameters for requesting the corresponding - // notebook HTML render - const updatedQS = updatedParameters - .map( - (item) => - `${encodeURIComponent(item[0])}=${encodeURIComponent(item[1])}` - ) - .join('&'); - return ( @@ -80,6 +75,7 @@ function TSNotebookViewer({ nbSlug, userParameters }) { From 538eb839f13652b892ff4d1ca52489a11837498b Mon Sep 17 00:00:00 2001 From: Jonathan Sick Date: Thu, 31 Mar 2022 19:26:08 -0400 Subject: [PATCH 6/6] Enhance README with dev primer The primer is based on the one written for Squareone. --- README.md | 2 - README.rst | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) delete mode 100644 README.md create mode 100644 README.rst diff --git a/README.md b/README.md deleted file mode 100644 index ff22471..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# times-square-ui -The web front-end for Times Square: parameterized notebooks as a service diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..cdc737a --- /dev/null +++ b/README.rst @@ -0,0 +1,119 @@ +.. image:: https://github.com/lsst-sqre/times-square-ui/actions/workflows/ci.yaml/badge.svg + :target: https://github.com/lsst-sqre/times-square-ui/actions/ + +############### +Times Square UI +############### + +**The front-end web interface for Times Square, a Rubin Science Platform (RSP) service for displaying parameterized Jupyter Notebooks as websites.** + +Excellent applications for Times Square include: + +- Engineering dashboards +- Quick-look data previewing +- Reports that incorporate live data sources + +The design and architecture of Times Square is described in `SQR-062: The Times Square service for publishing parameterized Jupyter Notebooks in the Rubin Science platform `__. +Times Square uses Noteburst (`GitHub `__, `SQR-065 `__ to execute Jupyter Notebooks in Nublado (JupyterLab) instances, thereby mechanizing the RSP's notebook aspect. + +This Times Square API service is developed separately at `https://github.com/lsst-sqre/times-square `__. +You can find the RSP deployment configuration in Phalanx's `services/times-square/ `__ directory. + +Technology stack +================ + +- The site is built with Next.js_ and React_. + Next.js_ allows the site to be dynamically configured for different Science Platform deployments. + +- Styling is done through styled-components_ (along with global CSS). + +Development workflow primer +=========================== + +Configure npm to use packages from @lsst-sqre +--------------------------------------------- + +Times Square UI uses npm packages published to the GitHub Package Registry in the ``lsst-sqre`` org. +Although they're publicly-available, you will need a `GitHub Personal Access Token `__ with ``read:packages``. + +Add an `@lsst-sqre` registry entry to your `~/.npmrc` file using the token you created:: + + @lsst-sqre:registry=https://npm.pkg.github.com/ + //npm.pkg.github.com/:_authToken=<...> + +Node version +------------ + +The Node.js version used by this this project is intended to be built with a Node.js version that's encoded in the `.nvmrc <./.nvmrc>`__ file. +To adopt this node version, we recommend `installing and using the node version manager `__. + +Then you can use the preferred node version by running ``nvm`` from the project root:: + + nvm use + +Install locally +--------------- + +Install the JavaScript packages:: + + npm install + +Install git hooks +----------------- + +Git hooks allow you to automatically lint and format code with eslint and prettier on each commit. +These hooks are managed by `husky `_, and should be installed automatically when you install Squareone locally. +If not, you can manually install the hooks:: + + husky install + +Manual linting and formatting +----------------------------- + +You can also manually lint and format code. + +Lint and format JavaScript via `next lint <>`__:: + + npm run lint + +Check formatting other types of code with Prettier_:: + + npm run format:check + +Or automatically fix files:: + + npm run format + +Start the development server +---------------------------- + +:: + + npm run dev + +View the site at http://localhost:3000/times-square/. +This site auto-updates when running with the development server. + +`API routes `_ are accessed on http://localhost:3000/times-square/api/*. +The ``pages/api`` directory is mapped to ``/api/*``. +Files in this directory are treated as `API routes`_ instead of React pages. +The purpose of the ``pages/api/dev`` endpoints are to mock external services in the RSP; see the re-writes in `next.config.js`. + +Create a production build +------------------------- + +This builds the optimized application:: + + npm run build + +You can serve the production build locally:: + + npm run serve + +.. _Next.js: https://nextjs.org +.. _Prettier: https://prettier.io/ +.. _Rubin Observatory: https://www.lsst.org +.. _React: https://reactjs.org +.. _styled-components: https://styled-components.com +.. _Semaphore: https://github.com/lsst-sqre/semaphore +.. _Phalanx: https://phalanx.lsst.io