From 67a9cb0c8a906f296c696e797d3ec51250b7c108 Mon Sep 17 00:00:00 2001 From: Taylor Steinberg Date: Fri, 31 Jan 2025 09:21:25 -0500 Subject: [PATCH] feat: publisher command center (#15) --- extensions/content-manager/.editorconfig | 10 + extensions/content-manager/.gitattributes | 4 + extensions/content-manager/.gitignore | 313 +++++ extensions/content-manager/.prettierignore | 5 + extensions/content-manager/.prettierrc | 1 + .../content-manager/.vscode/extensions.json | 3 + .../content-manager/.vscode/settings.json | 9 + extensions/content-manager/README.md | 102 ++ extensions/content-manager/app.py | 115 ++ extensions/content-manager/index.html | 12 + extensions/content-manager/package-lock.json | 1244 +++++++++++++++++ extensions/content-manager/package.json | 26 + extensions/content-manager/requirements.txt | 4 + extensions/content-manager/scss/index.scss | 29 + .../content-manager/src/components/About.js | 60 + .../content-manager/src/components/Author.js | 63 + .../src/components/ContentsComponent.js | 95 ++ .../src/components/Languages.js | 79 ++ .../content-manager/src/components/Metrics.js | 80 ++ .../src/components/Processes.js | 136 ++ .../src/components/Releases.js | 102 ++ extensions/content-manager/src/index.js | 25 + .../content-manager/src/models/Author.js | 34 + .../content-manager/src/models/Content.js | 34 + .../content-manager/src/models/Contents.js | 32 + .../content-manager/src/models/Metrics.js | 32 + .../content-manager/src/models/Process.js | 12 + .../content-manager/src/models/Processes.js | 41 + .../content-manager/src/models/Releases.js | 32 + extensions/content-manager/src/views/Edit.js | 84 ++ extensions/content-manager/src/views/Home.js | 20 + .../content-manager/src/views/Layout.js | 37 + extensions/content-manager/vite.config.js | 17 + 33 files changed, 2892 insertions(+) create mode 100644 extensions/content-manager/.editorconfig create mode 100644 extensions/content-manager/.gitattributes create mode 100644 extensions/content-manager/.gitignore create mode 100644 extensions/content-manager/.prettierignore create mode 100644 extensions/content-manager/.prettierrc create mode 100644 extensions/content-manager/.vscode/extensions.json create mode 100644 extensions/content-manager/.vscode/settings.json create mode 100644 extensions/content-manager/README.md create mode 100644 extensions/content-manager/app.py create mode 100644 extensions/content-manager/index.html create mode 100644 extensions/content-manager/package-lock.json create mode 100644 extensions/content-manager/package.json create mode 100644 extensions/content-manager/requirements.txt create mode 100644 extensions/content-manager/scss/index.scss create mode 100644 extensions/content-manager/src/components/About.js create mode 100644 extensions/content-manager/src/components/Author.js create mode 100644 extensions/content-manager/src/components/ContentsComponent.js create mode 100644 extensions/content-manager/src/components/Languages.js create mode 100644 extensions/content-manager/src/components/Metrics.js create mode 100644 extensions/content-manager/src/components/Processes.js create mode 100644 extensions/content-manager/src/components/Releases.js create mode 100644 extensions/content-manager/src/index.js create mode 100644 extensions/content-manager/src/models/Author.js create mode 100644 extensions/content-manager/src/models/Content.js create mode 100644 extensions/content-manager/src/models/Contents.js create mode 100644 extensions/content-manager/src/models/Metrics.js create mode 100644 extensions/content-manager/src/models/Process.js create mode 100644 extensions/content-manager/src/models/Processes.js create mode 100644 extensions/content-manager/src/models/Releases.js create mode 100644 extensions/content-manager/src/views/Edit.js create mode 100644 extensions/content-manager/src/views/Home.js create mode 100644 extensions/content-manager/src/views/Layout.js create mode 100644 extensions/content-manager/vite.config.js diff --git a/extensions/content-manager/.editorconfig b/extensions/content-manager/.editorconfig new file mode 100644 index 0000000..1ed453a --- /dev/null +++ b/extensions/content-manager/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{js,json,yml}] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/extensions/content-manager/.gitattributes b/extensions/content-manager/.gitattributes new file mode 100644 index 0000000..af3ad12 --- /dev/null +++ b/extensions/content-manager/.gitattributes @@ -0,0 +1,4 @@ +/.yarn/** linguist-vendored +/.yarn/releases/* binary +/.yarn/plugins/**/* binary +/.pnp.* binary linguist-generated diff --git a/extensions/content-manager/.gitignore b/extensions/content-manager/.gitignore new file mode 100644 index 0000000..925b532 --- /dev/null +++ b/extensions/content-manager/.gitignore @@ -0,0 +1,313 @@ +# Posit +.posit/ + +# Node +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# PyPI configuration file +.pypirc diff --git a/extensions/content-manager/.prettierignore b/extensions/content-manager/.prettierignore new file mode 100644 index 0000000..78e1938 --- /dev/null +++ b/extensions/content-manager/.prettierignore @@ -0,0 +1,5 @@ +# Ignore artifacts: +.posit +.venv +node_modules +dist diff --git a/extensions/content-manager/.prettierrc b/extensions/content-manager/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/extensions/content-manager/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/extensions/content-manager/.vscode/extensions.json b/extensions/content-manager/.vscode/extensions.json new file mode 100644 index 0000000..1453f7f --- /dev/null +++ b/extensions/content-manager/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["arcanis.vscode-zipfs"] +} diff --git a/extensions/content-manager/.vscode/settings.json b/extensions/content-manager/.vscode/settings.json new file mode 100644 index 0000000..dce71fb --- /dev/null +++ b/extensions/content-manager/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "search.exclude": { + "**/.yarn": true, + "**/.pnp.*": true + }, + "typescript.tsdk": ".yarn/sdks/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "cSpell.words": ["oninit", "onremove", "vnode"] +} diff --git a/extensions/content-manager/README.md b/extensions/content-manager/README.md new file mode 100644 index 0000000..f979598 --- /dev/null +++ b/extensions/content-manager/README.md @@ -0,0 +1,102 @@ + +# Publisher Command Center + +**Publisher Command Center** is a web-based application designed to help publishers manage and track their content. + +## Features + +- **Mithril.js** for lightweight and efficient UI rendering +- **Bootstrap** for responsive and consistent styling +- **FontAwesome** for scalable vector icons +- **Vite** for a fast development build system +- **FastAPI** as the backend framework + +## Getting Started + +### Prerequisites + +Ensure you have the following installed: + +- [Node.js](https://nodejs.org/) (for frontend development) +- [Python](https://www.python.org/) (for backend development) +- [uv](https://github.com/astral-sh/uv) (for running backend server) + +### Installation + +Clone the repository and install dependencies: + +```sh +npm install +``` + +### Running the Development Server + +Start the frontend development server: + +```sh +npm run preview +``` + +Start the backend development server: + +```sh +npm run server +``` + +Start the watcher to enable continuous rebuilds: + +```sh +npm run watch +``` + + + +### Building for Production + +To build the frontend for production: + +```sh +npm run build +``` + +## Technologies Used + +### [Mithril.js](https://mithril.js.org/) + +Mithril.js is a modern client-side JavaScript framework that focuses on simplicity and performance. It is lightweight (~10kb gzipped) and offers a virtual DOM implementation for efficient rendering. + +### [Bootstrap](https://getbootstrap.com/) + +Bootstrap is a popular CSS framework that helps create responsive and mobile-first designs with prebuilt components and utilities. + +### [FontAwesome](https://fontawesome.com/) + +FontAwesome provides scalable vector icons and social logos that can be used in web applications. + +### [Date-fns](https://date-fns.org/) + +Date-fns is a modern JavaScript date utility library that provides comprehensive, yet simple-to-use functions for date manipulation. + +### [Filesize.js](https://filesizejs.com/) + +Filesize.js is a small utility for formatting file sizes in human-readable formats. + +### [Vite](https://vitejs.dev/) + +Vite is a next-generation frontend tooling system that enables fast build times and instant development server start-up. + +### [SASS](https://sass-lang.com/) + +SASS (Syntactically Awesome Stylesheets) is a CSS preprocessor that extends CSS with features like variables, nested rules, and mixins. + +### [FastAPI](https://fastapi.tiangolo.com/) + +FastAPI is a modern web framework for building APIs with Python 3.7+ that provides automatic OpenAPI and JSON Schema documentation. + +## Code Formatting + +This project uses **Prettier** for consistent code formatting. Run the following command to format your code: + +```sh +npx prettier --write . +``` diff --git a/extensions/content-manager/app.py b/extensions/content-manager/app.py new file mode 100644 index 0000000..a24a924 --- /dev/null +++ b/extensions/content-manager/app.py @@ -0,0 +1,115 @@ +from http import client +import asyncio +from fastapi import FastAPI, Header +from fastapi.staticfiles import StaticFiles +from posit import connect + +from cachetools import TTLCache, cached + +client = connect.Client() + +app = FastAPI() + +# Create cache with TTL=1hour and unlimited size +client_cache = TTLCache(maxsize=float("inf"), ttl=3600) + + +@cached(client_cache) +def get_visitor_client(token: str | None) -> connect.Client: + """Create and cache API client per token with 1 hour TTL""" + if token: + return client.with_user_session_token(token) + else: + return client + + +@app.get("/api/contents") +async def contents(posit_connect_user_session_token: str = Header(None)): + visitor = get_visitor_client(posit_connect_user_session_token) + + response = client.get("metrics/procs") + processes = response.json() + + contents = visitor.me.content.find() + for content in contents: + content["processes"] = [ + process for process in processes if content["guid"] == process["app_guid"] + ] + + return contents + + +@app.get("/api/contents/{content_id}") +async def content( + content_id: str, posit_connect_user_session_token: str = Header(None) +): + visitor = get_visitor_client(posit_connect_user_session_token) + return visitor.content.get(content_id) + + +@app.get("/api/contents/{content_id}/processes") +async def get_content_processes( + content_id: str, posit_connect_user_session_token: str = Header(None) +): + visitor = get_visitor_client(posit_connect_user_session_token) + + # Assert the viewer has access to the content + assert visitor.content.get(content_id) + + response = client.get("metrics/procs") + processes = response.json() + + return [process for process in processes if process.get("app_guid") == content_id] + + +@app.delete("/api/contents/{content_id}/processes/{process_id}") +async def destroy_process( + content_id: str, + process_id: str, + posit_connect_user_session_token: str = Header(None), +): + visitor = get_visitor_client(posit_connect_user_session_token) + + content = visitor.content.get(content_id) + job = content.jobs.find(process_id) + if job: + job.destroy() + for _ in range(30): + job = content.jobs.find(process_id) + if job["status"] != 0: + return + await asyncio.sleep(1) + + +@app.get("/api/contents/{content_id}/author") +async def get_author( + content_id, + posit_connect_user_session_token: str = Header(None), +): + visitor = get_visitor_client(posit_connect_user_session_token) + content = visitor.content.get(content_id) + return content.owner + + +@app.get("/api/contents/{content_id}/releases") +async def get_releases( + content_id, + posit_connect_user_session_token: str = Header(None), +): + visitor = get_visitor_client(posit_connect_user_session_token) + content = visitor.content.get(content_id) + return content.bundles.find() + + +@app.get("/api/contents/{content_id}/metrics") +async def get_metrics( + content_id, + posit_connect_user_session_token: str = Header(None), +): + visitor = get_visitor_client(posit_connect_user_session_token) + content = visitor.content.get(content_id) + metrics = visitor.metrics.usage.find(content_guid=content["guid"]) + return metrics + + +app.mount("/", StaticFiles(directory="dist", html=True), name="static") diff --git a/extensions/content-manager/index.html b/extensions/content-manager/index.html new file mode 100644 index 0000000..6b4eb50 --- /dev/null +++ b/extensions/content-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Publisher Command Center + + +
+ + + diff --git a/extensions/content-manager/package-lock.json b/extensions/content-manager/package-lock.json new file mode 100644 index 0000000..1b3980d --- /dev/null +++ b/extensions/content-manager/package-lock.json @@ -0,0 +1,1244 @@ +{ + "name": "mithril-app", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mithril-app", + "version": "0.0.1", + "dependencies": { + "@fortawesome/fontawesome-free": "^6.7.2", + "@fortawesome/free-regular-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@popperjs/core": "^2.11.8", + "bootstrap": "^5.3.3", + "date-fns": "^4.1.0", + "filesize": "^10.1.6", + "mithril": "^2.0.4" + }, + "devDependencies": { + "prettier": "3.4.2", + "sass": "^1.83.4", + "vite": "^2.6.2" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz", + "integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", + "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/filesize": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", + "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 10.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mithril": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/mithril/-/mithril-2.2.12.tgz", + "integrity": "sha512-EpHb0DkcfwB+83NYvU8WMGBib+/TBtFLzdvYYXijqcW0o1i6cpVPklrCglJi2COXT2MG0Kih81CNPCoZkj0vag==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/readdirp": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.77.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz", + "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/sass": { + "version": "1.83.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", + "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/vite": { + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.18.tgz", + "integrity": "sha512-sAOqI5wNM9QvSEE70W3UGMdT8cyEn0+PmJMTFvTB8wB0YbYUWw3gUbY62AOyrXosGieF2htmeLATvNxpv/zNyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": ">=2.59.0 <2.78.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + } + } +} diff --git a/extensions/content-manager/package.json b/extensions/content-manager/package.json new file mode 100644 index 0000000..7b93c35 --- /dev/null +++ b/extensions/content-manager/package.json @@ -0,0 +1,26 @@ +{ + "name": "publisher-dashboard", + "version": "0.0.1", + "scripts": { + "dev": "vite", + "build": "vite build --base ./ --minify false", + "watch": "vite build --watch", + "preview": "vite preview", + "server": "uv run fastapi dev app.py" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "^6.7.2", + "@fortawesome/free-regular-svg-icons": "^6.7.2", + "@fortawesome/free-solid-svg-icons": "^6.7.2", + "@popperjs/core": "^2.11.8", + "bootstrap": "^5.3.3", + "date-fns": "^4.1.0", + "filesize": "^10.1.6", + "mithril": "^2.0.4" + }, + "devDependencies": { + "prettier": "3.4.2", + "sass": "^1.83.4", + "vite": "^2.6.2" + } +} diff --git a/extensions/content-manager/requirements.txt b/extensions/content-manager/requirements.txt new file mode 100644 index 0000000..92b8947 --- /dev/null +++ b/extensions/content-manager/requirements.txt @@ -0,0 +1,4 @@ +# requirements.txt auto-generated by Posit Publisher +# using /Users/me/Projects/connect-extensions/extensions/content-manager/.venv/bin/python +fastapi==0.115.6 +posit_sdk @ git+https://github.com/posit-dev/posit-sdk-py.git@main diff --git a/extensions/content-manager/scss/index.scss b/extensions/content-manager/scss/index.scss new file mode 100644 index 0000000..05efff5 --- /dev/null +++ b/extensions/content-manager/scss/index.scss @@ -0,0 +1,29 @@ +// index.scss + +$blue: #447099; +$indigo: #3d348b; +$purple: #9a4665; +$pink: #d96b94; +$red: #d44000; +$orange: #ee6331; +$yellow: #e7b10a; +$green: #72994e; +$teal: #419599; +$cyan: #53b0ae; +$gray: #404041; + +$colors: ( + "blue": $blue, + "indigo": $indigo, + "purple": $purple, + "pink": $pink, + "red": $red, + "orange": $orange, + "yellow": $yellow, + "green": $green, + "teal": $teal, + "cyan": $cyan, + "gray": $gray, +); + +@import "bootstrap/scss/bootstrap"; diff --git a/extensions/content-manager/src/components/About.js b/extensions/content-manager/src/components/About.js new file mode 100644 index 0000000..5f29764 --- /dev/null +++ b/extensions/content-manager/src/components/About.js @@ -0,0 +1,60 @@ +import m from "mithril"; + +import { format, formatDistanceToNow } from "date-fns"; + +import Content from "../models/Content"; + +const About = { + error: null, + + oninit: function (vnode) { + try { + Content.load(vnode.attrs.content_id); + } catch (err) { + this.error = "Failed to load author."; + console.error(err); + } + }, + + onremove: function () { + Content.reset(); + }, + + view: function (vnode) { + if (this.error) { + return m("div", { class: "error" }, this.error); + } + + const content = Content.data; + if (content === null) { + return ""; + } + + const desc = content?.description; + const updated = content?.last_deployed_time; + const created = content?.created_time; + + return m(".pt-3.border-top", [ + m(".", [ + m("h5", "About"), + m("p", desc || m("i", "No Description")), + m( + "p", + m( + "small.text-body-secondary", + "Updated " + formatDistanceToNow(updated, { addSuffix: true }), + ), + ), + m( + "p", + m( + "small.text-body-secondary", + "Created on " + format(created, "MMMM do, yyyy"), + ), + ), + ]), + ]); + }, +}; + +export default About; diff --git a/extensions/content-manager/src/components/Author.js b/extensions/content-manager/src/components/Author.js new file mode 100644 index 0000000..043f90f --- /dev/null +++ b/extensions/content-manager/src/components/Author.js @@ -0,0 +1,63 @@ +import m from "mithril"; + +import { formatDistanceToNow } from "date-fns"; + +import Author from "../models/Author"; + +export default { + error: null, + + oninit: function (vnode) { + try { + Author.load(vnode.attrs.content_id); + } catch (err) { + this.error = "Failed to load author."; + console.error(err); + } + }, + + onremove: function () { + Author.reset(); + }, + + view: function (vnode) { + if (this.error) { + return m("div", { class: "error" }, this.error); + } + + const author = Author.data; + if (author === null) { + return ""; + } + + return m(".pt-3.border-top", [ + m(".", [ + m("h5", "Author"), + m("p", author?.first_name + " " + author?.last_name), + m( + "p", + m("small.text-body-secondary.align-items-center", [ + m(".fa-regular.fa-at"), + " ", + author?.username, + ]), + ), + m( + "p", + m("small.text-body-secondary.align-items-center", [ + m(".fa-regular.fa-envelope"), + " ", + m("a", { href: `mailto:${author?.email}` }, author?.email), + ]), + ), + m( + "p", + m("small.text-body-secondary", [ + "Active ", + formatDistanceToNow(author?.active_time, { addSuffix: true }), + ]), + ), + ]), + ]); + }, +}; diff --git a/extensions/content-manager/src/components/ContentsComponent.js b/extensions/content-manager/src/components/ContentsComponent.js new file mode 100644 index 0000000..ae7e9db --- /dev/null +++ b/extensions/content-manager/src/components/ContentsComponent.js @@ -0,0 +1,95 @@ +import m from "mithril"; +import { format } from "date-fns"; +import Contents from "../models/Contents"; +import Languages from "./Languages"; + +const ContentsComponent = { + error: null, + + oninit: function () { + try { + Contents.load(); + } catch (err) { + this.error = "Failed to load data."; + console.error(err); + } + }, + + view: () => { + if (this.error) { + return m("div", { class: "error" }, this.error); + } + + const contents = Contents.data; + if (contents === null) { + return; + } + + if (contents.length === 0) { + return ""; + } + + return m( + "table", + { class: "table" }, + m( + "thead", + m("tr", [ + m("th", { scope: "col" }, ""), + m("th", { scope: "col" }, "Title"), + m("th", { scope: "col" }, "Language"), + m("th", { scope: "col" }, "Running Processes"), + m("th", { scope: "col" }, "Last Updated"), + m("th", { scope: "col" }, "Date Added"), + m("th", { scope: "col" }, ""), + ]), + ), + m( + "tbody", + Contents.data.map((content) => { + const guid = content["guid"]; + return m( + "tr", + { + style: { cursor: "pointer" }, + onclick: () => m.route.set(`/contents/${guid}`), + }, + [ + m( + "td", + m("", { + class: "fa-solid fa-gear text-secondary", + }), + ), + m( + "td", + m( + ".link-primary", + content["title"] || m("i", "No Name"), + ), + ), + m( + "td", + m(Languages, content) + ), + m("td", content?.processes.length), + m("td", format(content["last_deployed_time"], "MMM do, yyyy")), + m("td", format(content["created_time"], "MMM do, yyyy")), + m( + "td", + m("a", { + class: "fa-solid fa-arrow-up-right-from-square", + href: content["content_url"], + target: "_blank", + onclick: (e) => e.stopPropagation(), + }), + ), + ], + ); + }), + ), + ); + }, +}; + +export default ContentsComponent; diff --git a/extensions/content-manager/src/components/Languages.js b/extensions/content-manager/src/components/Languages.js new file mode 100644 index 0000000..3244ee6 --- /dev/null +++ b/extensions/content-manager/src/components/Languages.js @@ -0,0 +1,79 @@ +import m from "mithril"; + +// api - R code defining a Plumber API. + +// jupyter-voila A Voila interactive dashboard. + +const getLanguages = (content) => { + const languages = new Set(); + + switch (content?.app_mode) { + case "jupyter-static": + case "jupyter-static": + case "python-api": + case "python-bokeh": + case "python-dash": + case "python-fastapi": + case "python-gradio": + case "python-shiny": + case "python-streamlit": + case "tensorflow-saved-model": + languages.add("Python"); + break; + case "quarto-shiny": + case "quarto-static": + languages.add("Quarto"); + break; + case "rmd-shiny": + case "rmd-static": + case "shiny": + languages.add("R"); + break; + case "static": + languages.add("Static"); + break; + default: + break; + } + + switch (content?.content_category) { + case "plot": + languages.add("Static") + languages.add("Plot"); + break; + case "pin": + languages.add("Static"); + languages.add("Pin") + break; + case "rmd-static": + languages.add("Static") + languages.add("Site") + break; + default: + break; + } + + if (content["r_version"] != null && content["r_version"] !== "") { + languages.add("R"); + } + if (content["py_version"] != null && content["py_version"] !== "") { + languages.add("Python"); + } + if (content["quarto_version"] != null && content["quarto_version"] !== "") { + languages.add("Quarto"); + } + if (content["content_category"] === "pin") { + languages.add("Pin"); + } + + return [...languages].sort(); +}; + +export default { + view: function (vnode) { + const languages = getLanguages(vnode.attrs); + return languages.map((language) => { + return m("span", { class: "mx-1 badge text-bg-primary" }, language); + }); + }, +}; diff --git a/extensions/content-manager/src/components/Metrics.js b/extensions/content-manager/src/components/Metrics.js new file mode 100644 index 0000000..00deaba --- /dev/null +++ b/extensions/content-manager/src/components/Metrics.js @@ -0,0 +1,80 @@ +import m from "mithril"; +import Metrics from "../models/Metrics"; + +const TimeseriesChart = { + oncreate: function (vnode) { + // Initialize the chart when the component is created + const ctx = vnode.dom.getContext('2d'); + const labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']; + const data = [10, 20, 15, 25, 30, 20]; + + new Chart(ctx, { + type: 'line', + data: { + labels: labels, + datasets: [{ + label: 'Views', + data: data, + borderColor: 'rgba(75, 192, 192, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + tension: 0.4 // Smooth line + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + title: { + display: true, + text: 'Month' + } + }, + y: { + title: { + display: true, + text: 'Value' + }, + beginAtZero: true + } + } + } + }); + }, + view: function () { + return m('canvas', { style: 'width: 100%; height: 400px;' }); + } +}; + +export default { + error: null, + + oninit: function (vnode) { + try { + Metrics.load(vnode.attrs.id); + } catch (err) { + this.error = "Failed to load data."; + console.error(err); + } + }, + + view: function (vnode) { + if (this.error) { + return m("div", { class: "error" }, this.error); + } + + const metrics = Metrics.data; + if (metrics === null) { + return; + } + + const start = Math.min(...metrics.map(metric => new Date(metric?.started))) + const ended = Math.max(...metrics.map(metric => new Date(metric?.ended))) + console.log(start, ended) + + return m(".pt-3.border-top", [ + m("h5", "Metrics"), + m(TimeseriesChart), + ]); + }, +}; diff --git a/extensions/content-manager/src/components/Processes.js b/extensions/content-manager/src/components/Processes.js new file mode 100644 index 0000000..3a0f154 --- /dev/null +++ b/extensions/content-manager/src/components/Processes.js @@ -0,0 +1,136 @@ +import m from "mithril"; +import { formatDistanceToNow } from "date-fns"; +import { filesize } from "filesize"; +import Processes from "../models/Processes"; +import Process from "../models/Process"; + +const StopButton = { + oninit(vnode) { + vnode.state.isHovered = false; + vnode.state.disabled = false; + }, + + view(vnode) { + return m( + "button", + { + class: "btn btn-link text-danger p-0", + disabled: vnode.state.disabled, + onclick: () => { + if (vnode.state.disabled) { + return; + } + + vnode.state.disabled = true; + m.redraw(); + + console.log(`Stopping process ${vnode.attrs.process_id}`); + Processes.destroy( + vnode.attrs.content_id, + vnode.attrs.process_id + ) + .then(() => { + console.log(`Stopped process ${vnode.attrs.process_id}`); + Processes.reset(); + return Processes.load(); + }) + .then(() => { + m.redraw(); // Trigger UI refresh after reload + }) + .catch((err) => { + console.error("Failed to reload processes:", err); + vnode.state.disabled = false; // Re-enable button on error + m.redraw(); + }); + }, + title: "Stop Process", + onmouseover: () => { + vnode.state.isHovered = true; + m.redraw(); + }, + onmouseout: () => { + vnode.state.isHovered = false; + m.redraw(); + }, + }, + m( + `i.${vnode.state.isHovered ? "fa-solid" : "fa-regular"}.fa-circle-stop`, + { + style: "font-size: 1.2rem;", + } + ) + ); + }, +}; + + +export default { + error: null, + + oninit: function (vnode) { + try { + Processes.load(vnode.attrs.id); + } catch (err) { + this.error = "Failed to load data."; + console.error(err); + } + }, + + onremove: function (vnode) { + Processes.reset(); + }, + + view: function (vnode) { + if (this.error) { + return m("div", { class: "error" }, this.error); + } + + const processes = Processes.data; + if (processes === null || processes.length === 0) { + return m(".pt-3.border-top", [ + m("h5", "Processes"), + m("p.text-dark", "There are no server processes running at this time...") + ]) + } + + return m(".pt-3.border-top", [ + m("h5", "Processes"), + m( + "table.table", + m( + "thead", + m("tr", [ + m("th", { scope: "col" }, ""), + m("th", { scope: "col" }, "Id"), + m("th", { scope: "col" }, "Started"), + m("th", { scope: "col" }, "CPUs"), + m("th", { scope: "col" }, "Memory"), + m("th", { scope: "col" }, "Hostname"), + ]), + ), + m( + "tbody", + processes.map((process) => { + return m("tr.align-items-center", [ + m( + "td.text-center.py-2", + m(StopButton, { + content_id: vnode.attrs.id, + process_id: process?.job_key, + }), + ), + m("td", process?.pid), + m( + "td", + formatDistanceToNow(process?.start_time, { addSuffix: true }), + ), + m("td", Number(process?.cpu_current).toFixed(2)), + m("td", filesize(process?.ram)), + m("td", process?.hostname), + ]); + }), + ), + ), + ]); + }, +}; diff --git a/extensions/content-manager/src/components/Releases.js b/extensions/content-manager/src/components/Releases.js new file mode 100644 index 0000000..c5bae7f --- /dev/null +++ b/extensions/content-manager/src/components/Releases.js @@ -0,0 +1,102 @@ +import m from "mithril"; + +import { format } from "date-fns"; + +import Releases from "../models/Releases"; + +const Release = { + view: function (vnode) { + return m(".row.my-3", [ + m(".d-flex.align-items-center", [ + m("i.fa-solid.fa-code-commit.me-1"), + m(".font-monospace", vnode.attrs?.id), + vnode.attrs?.active + ? m("i.fa-regular.fa-circle-check.ms-1.text-success") + : "", + ]), + m( + "small.text-secondary", + format(vnode.attrs?.created_time, "MMM do, yyyy"), + ), + ]); + }, +}; + +export default { + error: null, + + oninit: function (vnode) { + try { + Releases.load(vnode.attrs.content_id); + } catch (err) { + this.error = "Failed to load releases."; + console.error(err); + } + }, + + onremove: function () { + Releases.reset(); + }, + + view: function () { + if (this.error) { + return m("div", { class: "error" }, this.error); + } + + const releases = Releases.data; + if (releases === null) { + return; + } + + if (releases.length === 0) { + return; + } + + let recent = releases.slice(0, 3); + recent = recent.map((release) => { + return m(Release, release); + }); + + let old = releases.slice(3); + old = old.map((release) => { + return m(Release, release); + }); + if (old.length === 0) { + old = ""; + } else { + old = [ + m( + "", + { + class: "text-secondary", + type: "button", + "data-bs-toggle": "collapse", + "data-bs-target": "#old", + onclick: (e) => { + const icon = e.target.querySelector("i") || e.target; + const currentRotation = + icon.style.transform === "rotate(90deg)" + ? "rotate(0deg)" + : "rotate(90deg)"; + icon.style.transform = currentRotation; + icon.style.transition = "transform 0.3s ease"; + }, + }, + m("i", { class: "fa-solid fa-ellipsis" }), + ), + m("div", { class: "collapse", id: "old" }, old), + ]; + } + + return m(".pt-3.border-top", [ + m(".", [ + m("h5", [ + "Releases ", + m("span.badge.rounded-pill.text-bg-secondary", releases.length), + ]), + recent, + old, + ]), + ]); + }, +}; diff --git a/extensions/content-manager/src/index.js b/extensions/content-manager/src/index.js new file mode 100644 index 0000000..42103c8 --- /dev/null +++ b/extensions/content-manager/src/index.js @@ -0,0 +1,25 @@ +import m from "mithril"; + +import "bootstrap/dist/js/bootstrap.bundle.min.js"; +import "@fortawesome/fontawesome-free/css/all.min.css"; + +import "../scss/index.scss"; + + +import Home from "./views/Home"; +import Edit from "./views/Edit"; +import Layout from './views/Layout' + +const root = document.getElementById("app"); +m.route(root, "/contents", { + "/contents": { + render: function () { + return m(Layout, m(Home)) + }, + }, + "/contents/:id": { + render: function (vnode) { + return m(Layout, m(Edit, vnode.attrs)); + }, + }, +}); diff --git a/extensions/content-manager/src/models/Author.js b/extensions/content-manager/src/models/Author.js new file mode 100644 index 0000000..934cc57 --- /dev/null +++ b/extensions/content-manager/src/models/Author.js @@ -0,0 +1,34 @@ +import m from "mithril"; + +const Author = { + data: null, + _fetch: null, + + load: function (id) { + if (this.data) { + return Promise.resolve(this.data); + } + + if (this._fetch) { + return this._fetch; + } + + this._fetch = m + .request({ method: "GET", url: `api/contents/${id}/author` }) + .then((result) => { + this.data = result; + this._fetch = null; + }) + .catch((err) => { + this._fetch = null; + throw err; + }); + }, + + reset: function () { + this.data = null; + this._fetch = null; + }, +}; + +export default Author; diff --git a/extensions/content-manager/src/models/Content.js b/extensions/content-manager/src/models/Content.js new file mode 100644 index 0000000..a3599d6 --- /dev/null +++ b/extensions/content-manager/src/models/Content.js @@ -0,0 +1,34 @@ +import m from "mithril"; + +const Content = { + data: null, + _fetch: null, + + load: function (id) { + if (this.data) { + return Promise.resolve(this.data); + } + + if (this._fetch) { + return this._fetch; + } + + this._fetch = m + .request({ method: "GET", url: `api/contents/${id}` }) + .then((result) => { + this.data = result; + this._fetch = null; + }) + .catch((err) => { + this._fetch = null; + throw err; + }); + }, + + reset: function () { + this.data = null; + this._fetch = null; + }, +}; + +export default Content; diff --git a/extensions/content-manager/src/models/Contents.js b/extensions/content-manager/src/models/Contents.js new file mode 100644 index 0000000..3c51a91 --- /dev/null +++ b/extensions/content-manager/src/models/Contents.js @@ -0,0 +1,32 @@ +import m from "mithril"; + +export default { + data: null, + _fetch: null, + + load: function (id) { + if (this.data) { + return Promise.resolve(this.data); + } + + if (this._fetch) { + return this._fetch; + } + + this._fetch = m + .request({ method: "GET", url: `api/contents` }) + .then((result) => { + this.data = result; + this._fetch = null; + }) + .catch((err) => { + this._fetch = null; + throw err; + }); + }, + + reset: function () { + this.data = null; + this._fetch = null; + }, +}; diff --git a/extensions/content-manager/src/models/Metrics.js b/extensions/content-manager/src/models/Metrics.js new file mode 100644 index 0000000..75159ac --- /dev/null +++ b/extensions/content-manager/src/models/Metrics.js @@ -0,0 +1,32 @@ +import m from "mithril"; + +export default { + data: null, + _fetch: null, + + load: function (id) { + if (this.data) { + return Promise.resolve(this.data); + } + + if (this._fetch) { + return this._fetch; + } + + this._fetch = m + .request({ method: "GET", url: `api/contents/${id}/metrics` }) + .then((result) => { + this.data = result; + this._fetch = null; + }) + .catch((err) => { + this._fetch = null; + throw err; + }); + }, + + reset: function () { + this.data = null; + this._fetch = null; + }, +}; diff --git a/extensions/content-manager/src/models/Process.js b/extensions/content-manager/src/models/Process.js new file mode 100644 index 0000000..49afba1 --- /dev/null +++ b/extensions/content-manager/src/models/Process.js @@ -0,0 +1,12 @@ +import m from "mithril"; + +const Process = { + destroy: function (content_id, process_id) { + return m.request({ + method: "DELETE", + url: `api/contents/${content_id}/processes/${process_id}`, + }); + }, +}; + +export default Process; diff --git a/extensions/content-manager/src/models/Processes.js b/extensions/content-manager/src/models/Processes.js new file mode 100644 index 0000000..9dcce93 --- /dev/null +++ b/extensions/content-manager/src/models/Processes.js @@ -0,0 +1,41 @@ +import m from "mithril"; + +const Processes = { + data: null, + _fetch: null, + + load: function (id) { + if (this.data) { + return Promise.resolve(this.data); + } + + if (this._fetch) { + return this._fetch; + } + + this._fetch = m + .request({ method: "GET", url: `api/contents/${id}/processes` }) + .then((result) => { + this.data = result; + this._fetch = null; + }) + .catch((err) => { + this._fetch = null; + throw err; + }); + }, + + destroy: function (content_id, process_id) { + return m.request({ + method: "DELETE", + url: `api/contents/${content_id}/processes/${process_id}`, + }); + }, + + reset: function () { + this.data = null; + this._fetch = null; + }, +}; + +export default Processes; diff --git a/extensions/content-manager/src/models/Releases.js b/extensions/content-manager/src/models/Releases.js new file mode 100644 index 0000000..843a07b --- /dev/null +++ b/extensions/content-manager/src/models/Releases.js @@ -0,0 +1,32 @@ +import m from "mithril"; + +export default { + data: null, + _fetch: null, + + load: function (id) { + if (this.data) { + return Promise.resolve(this.data); + } + + if (this._fetch) { + return this._fetch; + } + + this._fetch = m + .request({ method: "GET", url: `api/contents/${id}/releases` }) + .then((result) => { + this.data = result; + this._fetch = null; + }) + .catch((err) => { + this._fetch = null; + throw err; + }); + }, + + reset: function () { + this.data = null; + this._fetch = null; + }, +}; diff --git a/extensions/content-manager/src/views/Edit.js b/extensions/content-manager/src/views/Edit.js new file mode 100644 index 0000000..fed5be1 --- /dev/null +++ b/extensions/content-manager/src/views/Edit.js @@ -0,0 +1,84 @@ +import m from "mithril"; + +import Content from "../models/Content"; +import About from "../components/About"; +import Releases from "../components/Releases"; +import Processes from "../components/Processes"; +import Author from "../components/Author"; + +import Languages from "../components/Languages"; + +const Edit = { + error: null, + + oninit: function (vnode) { + try { + Content.load(vnode.attrs.id); + } catch (err) { + this.error = "Failed to load data."; + console.error(err); + } + }, + + onremove: function () { + Content.reset(); + }, + + view: function (vnode) { + if (this.error) { + return m("div", { class: "error" }, this.error); + } + + const content = Content.data; + if (content === null) { + return ""; + } + + return m( + "div", + m(".d-flex.flex-row.justify-content-between.align-items-center.my-3", [ + m("h2.mb-0", content?.title || m("i", "No Name")), + m( + "a.btn.btn-lg.btn-outline-primary.d-flex.align-items-center.justify-content-center.gap-2", + { + href: content?.dashboard_url, + target: "_blank", + }, + ["Open in Connect", m("i.fa-solid.fa-arrow-up-right-from-square")], + ), + ]), + m(".row", m(".pb-3", m(Languages, content))), + m(".row.", [ + m(".col-8", [ + m( + ".pt-3.pb-3.border-top", + m("iframe", { + src: content?.content_url, + width: "100%", + style: { minHeight: "50vh" }, + frameborder: "0", + allowfullscreen: true, + class: "border border-bottom-0 border-light rounded p-1", + }), + ), + m(Processes, { id: vnode.attrs.id }), + ]), + m(".col-4", [ + m(About, { + desc: content?.description, + updated: content?.last_deployed_time, + created: content?.created_time, + }), + m(Author, { + content_id: content?.guid, + }), + m(Releases, { + content_id: content?.guid, + }), + ]), + ]), + ); + }, +}; + +export default Edit; diff --git a/extensions/content-manager/src/views/Home.js b/extensions/content-manager/src/views/Home.js new file mode 100644 index 0000000..a299eb0 --- /dev/null +++ b/extensions/content-manager/src/views/Home.js @@ -0,0 +1,20 @@ +import m from "mithril"; + +import ContentsComponent from "../components/ContentsComponent"; + +const Home = { + view: function () { + return m( + "div", + m("h1", "Content"), + m( + "p", + { class: "text-secondary" }, + "Manage your content and their settings here.", + ), + m(ContentsComponent), + ); + }, +}; + +export default Home; diff --git a/extensions/content-manager/src/views/Layout.js b/extensions/content-manager/src/views/Layout.js new file mode 100644 index 0000000..aec6953 --- /dev/null +++ b/extensions/content-manager/src/views/Layout.js @@ -0,0 +1,37 @@ +import m from "mithril"; + +export default { + view: (vnode) => { + return m("div", [ + // Navbar Header + m("nav.navbar.navbar-expand-lg.bg-light", [ + m("div.container-xxl", [ + m( + "a.navbar-brand", + { + style: { cursor: "pointer" }, + onclick: () => m.route.set(`/`), + }, + "Publisher Command Center", + ), + m("ul.navbar-nav.me-auto", [ + m( + "li.nav-item", + m( + "a.nav-link", + { + style: { cursor: "pointer" }, + onclick: () => m.route.set("/contents"), + }, + "Content", + ), + ), + ]), + ]), + ]), + + // Content Wrapper + m("div.container-xxl", vnode.children), + ]); + }, +}; diff --git a/extensions/content-manager/vite.config.js b/extensions/content-manager/vite.config.js new file mode 100644 index 0000000..3a89c85 --- /dev/null +++ b/extensions/content-manager/vite.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + esbuild: { + jsx: "transform", + jsxFactory: "m", + jsxFragment: "'['", + }, + preview: { + proxy: { + "/api": { + target: "http://localhost:8000", + changeOrigin: true, + }, + }, + }, +});