diff --git a/.github/workflows/manual-gh-pages-deploy.yml b/.github/workflows/gh_pages_deploy.yml similarity index 59% rename from .github/workflows/manual-gh-pages-deploy.yml rename to .github/workflows/gh_pages_deploy.yml index 598e60f7f0c..74addce378d 100644 --- a/.github/workflows/manual-gh-pages-deploy.yml +++ b/.github/workflows/gh_pages_deploy.yml @@ -1,6 +1,9 @@ -name: Manual Deploy to GitHub Pages +name: Deploy to GitHub Pages on: + push: + branches: + - 'main' workflow_dispatch: concurrency: @@ -10,6 +13,8 @@ concurrency: jobs: build_assets: runs-on: ubuntu-20.04 + outputs: + should_deploy: ${{ steps.check_deploy.outputs.should_deploy }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -46,7 +51,23 @@ jobs: shell: bash run: npm run build + - name: Check if deploy needed + id: check_deploy + uses: actions/github-script@v7 + with: + script: | + const { readFile } = require('fs/promises'); + const { checksumFileName } = await import('${{ github.workspace }}/selector/src/shared/build-checksum.js'); + const { owner, repo } = context.repo; + const { data: { html_url }} = await github.rest.repos.getPages({ owner, repo }); + const deployedChecksum = await fetch(`${html_url}/${checksumFileName}`).then((res) => res.status === 200 ? res.text() : null); + const currentChecksum = await readFile(`${{ github.workspace }}/selector/dist/openvino_notebooks/${checksumFileName}`, { encoding: 'utf8'}); + const isManualDeploy = context.eventName === 'workflow_dispatch'; + const shouldDeploy = isManualDeploy || currentChecksum !== deployedChecksum; + core.setOutput('should_deploy', shouldDeploy); + - name: Upload pages artifact + if: ${{ steps.check_deploy.outputs.should_deploy == 'true' }} uses: actions/upload-pages-artifact@v3 with: path: ./selector/dist/openvino_notebooks @@ -54,6 +75,7 @@ jobs: deploy_github_pages: runs-on: ubuntu-20.04 needs: build_assets + if: ${{ needs.build_assets.outputs.should_deploy == 'true' }} permissions: pages: write id-token: write diff --git a/selector/src/components/ContentSection/NotebooksList/NotebookCard/NotebookCard.scss b/selector/src/components/ContentSection/NotebooksList/NotebookCard/NotebookCard.scss index 9d0d59504f7..f948dd4d60e 100644 --- a/selector/src/components/ContentSection/NotebooksList/NotebookCard/NotebookCard.scss +++ b/selector/src/components/ContentSection/NotebooksList/NotebookCard/NotebookCard.scss @@ -7,6 +7,12 @@ &.clickable { cursor: pointer; + + &:hover { + .spark-card-horizontal-title { + text-decoration: underline; + } + } } &:hover { diff --git a/selector/src/components/FiltersPanel/FiltersPanel.scss b/selector/src/components/FiltersPanel/FiltersPanel.scss index 3aeee1fd93a..ed6b17e77f8 100644 --- a/selector/src/components/FiltersPanel/FiltersPanel.scss +++ b/selector/src/components/FiltersPanel/FiltersPanel.scss @@ -66,4 +66,12 @@ z-index: 100; overflow-y: auto; } + + body.embedded { + .filters-panel-footer { + margin-top: -2.5rem; + position: initial; + padding: 1rem; + } + } } diff --git a/selector/src/notebook-metadata/generate-notebooks-map.js b/selector/src/notebook-metadata/generate-notebooks-map.js index 7be5bfb06bf..dff47e9c899 100644 --- a/selector/src/notebook-metadata/generate-notebooks-map.js +++ b/selector/src/notebook-metadata/generate-notebooks-map.js @@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { join, resolve } from 'path'; +import { createBuildChecksumFile } from '../shared/build-checksum.js'; import { NotebookMetadataValidationError } from './notebook-metadata-validator.js'; export const NOTEBOOKS_MAP_FILE_NAME = 'notebooks-metadata-map.json'; @@ -65,6 +66,7 @@ export const generateNotebooksMapFilePlugin = () => { async closeBundle() { if (config.command === 'build') { await generateNotebooksMapFile(distPath); + await createBuildChecksumFile(distPath); } }, async configureServer(devServer) { diff --git a/selector/src/shared/build-checksum.js b/selector/src/shared/build-checksum.js new file mode 100644 index 00000000000..20afc4f2292 --- /dev/null +++ b/selector/src/shared/build-checksum.js @@ -0,0 +1,52 @@ +// @ts-check + +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import { join, resolve } from 'node:path'; + +const algorithm = 'sha256'; + +export const checksumFileName = `checksum.${algorithm}`; + +const createSHAHash = () => crypto.createHash(algorithm); + +/** + * @param {string} distPath + * @returns {Promise} + */ +export async function createBuildChecksumFile(distPath) { + const paths = await getFilesInDirectory(distPath); + const fileChecksums = await Promise.all(paths.map(async (path) => await getFileChecksum(path))); + const buildChecksum = createSHAHash().update(fileChecksums.join('')).digest('hex'); + await fs.promises.writeFile(join(distPath, checksumFileName), buildChecksum, { flag: 'w' }); +} + +/** + * @param {string} filePath + * @returns {Promise} + */ +async function getFileChecksum(filePath) { + return new Promise((resolve, reject) => { + const hash = createSHAHash(); + const stream = fs.createReadStream(filePath); + stream.on('error', (err) => reject(err)); + stream.on('data', (chunk) => hash.update(chunk)); + stream.on('end', () => resolve(hash.digest('hex'))); + }); +} + +/** + * @param {string} directoryPath + * @returns {Promise} + */ +async function getFilesInDirectory(directoryPath) { + const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true }); + const entriesPromises = entries.map(async (entry) => { + const entryPath = resolve(directoryPath, entry.name); + if (entry.isDirectory()) { + return await getFilesInDirectory(entryPath); + } + return entryPath; + }); + return (await Promise.all(entriesPromises)).flat(20); +} diff --git a/selector/src/shared/iframe-message-emitter.ts b/selector/src/shared/iframe-message-emitter.ts index dc1cf0119b1..2424656e3e3 100644 --- a/selector/src/shared/iframe-message-emitter.ts +++ b/selector/src/shared/iframe-message-emitter.ts @@ -1,3 +1,5 @@ +import { isEmbedded } from './iframe-detector'; + export interface IResizeMessage { type: 'resize'; height: number; @@ -11,7 +13,7 @@ export const sendScrollMessage = (): void => { const message: IScrollMessage = { type: 'scroll', }; - window.parent.postMessage(message); + window.parent.postMessage(message, '*'); }; const report = () => { @@ -19,7 +21,11 @@ const report = () => { type: 'resize', height: document.body.offsetHeight, }; - window.parent.postMessage(message); + window.parent.postMessage(message, '*'); }; new ResizeObserver(report).observe(document.body); + +if (isEmbedded) { + document.body.classList.add('embedded'); +} diff --git a/selector/src/shared/iframe-message-handler.ts b/selector/src/shared/iframe-message-handler.ts index a285c7d6072..a8496a27a13 100644 --- a/selector/src/shared/iframe-message-handler.ts +++ b/selector/src/shared/iframe-message-handler.ts @@ -18,7 +18,11 @@ function setInitialIframeHeight(iframeElement: HTMLIFrameElement): void { } window.onmessage = (message: MessageEvent) => { - if (message.origin !== window.origin) { + const { origin: allowedOrigin } = new URL( + import.meta.env.PROD ? (import.meta.env.VITE_APP_LOCATION as string) : import.meta.url + ); + + if (message.origin !== allowedOrigin) { return; } diff --git a/selector/src/styles/styles.scss b/selector/src/styles/styles.scss index d3d4a726166..119fc55db79 100644 --- a/selector/src/styles/styles.scss +++ b/selector/src/styles/styles.scss @@ -4,6 +4,10 @@ body { margin: 0; @include flex-col(); + + &.embedded { + overflow-y: hidden; + } } #root { diff --git a/selector/vite.config.ts b/selector/vite.config.ts index b96748f7efc..13ae20659c3 100644 --- a/selector/vite.config.ts +++ b/selector/vite.config.ts @@ -33,6 +33,14 @@ export default defineConfig(({ mode }) => { index: resolve(__dirname, 'index.html'), embedded: resolve(__dirname, 'embedded.html'), }, + output: { + entryFileNames({ name, isEntry }) { + if (name === 'embedded' && isEntry) { + return 'assets/[name].js'; + } + return 'assets/[name]-[hash].js'; + }, + }, }, }, envDir: ENV_DIR,