diff --git a/.github/workflows/build-push-aip-webapp-server-docker-images.yml b/.github/workflows/build-push-aip-webapp-server-docker-images.yml new file mode 100644 index 000000000..b95d62a5a --- /dev/null +++ b/.github/workflows/build-push-aip-webapp-server-docker-images.yml @@ -0,0 +1,106 @@ +name: Create and publish the webapp server docker images for AIP +on: + workflow_dispatch: + inputs: + targetRef: + description: 'Tag, branch or commit from which to build the server image' + default: '' + required: true + type: string + imageTag: + description: 'Tag to set when building the docker image' + default: '' + required: false + type: string + push: + # List of branches that will trigger the workflow + branches: ['feature/aip'] + +env: + LATEST_BRANCH: 'feature/aip' + DOCKER_BUILDKIT: 1 + # Change the Container Registry information below if necessary + REGISTRY: ghcr.io + REPOSITORY_NAME: ${{ github.repository }} + # PUBLIC_URL is only required when building the server in instance-specific mode + #PUBLIC_URL: /cosmotech-webapp/brewery + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout git repository + uses: actions/checkout@v5 + with: + # Use workflow input "targetRef" if provided, or the branch/commit/tag that triggered the job otherwise + ref: ${{ inputs.targetRef != '' && inputs.targetRef || github.ref_name }} + - name: Enable Corepack + run: corepack enable + - name: Setup node + uses: actions/setup-node@v5 + with: + node-version: 24 + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: "webapp-server: extract Docker metadata (tags, labels)" + id: webapp-server-metadata + uses: docker/metadata-action@v5 + with: + # Change the tag and image name below if needed (see https://github.com/docker/metadata-action) + flavor: | + latest=false + tags: | + # set latest tag only for one branch + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', env.LATEST_BRANCH) }},priority=1200 + type=raw,value=${{ inputs.imageTag }},enable=${{ inputs.imageTag != '' }},priority=1100 + type=ref,event=branch + type=ref,event=tag + images: ${{ env.REGISTRY }}/${{ env.REPOSITORY_NAME }}/webapp-server-aip + - name: "webapp-server: build and push Docker image" + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_RECORD_UPLOAD: false + with: + context: . + file: webapp_server/webapp-server.Dockerfile + push: true + tags: ${{ steps.webapp-server-metadata.outputs.tags }} + labels: ${{ steps.webapp-server-metadata.outputs.labels }} + # Target stage can be either server-universal or server-specific (default) + target: server-universal + # PUBLIC_URL is only required when building the server in instance-specific mode + #build-args: | + # "PUBLIC_URL=${{ env.PUBLIC_URL }}" + + - name: "webapp-functions: extract Docker metadata (tags, labels)" + id: webapp-functions-metadata + uses: docker/metadata-action@v5 + with: + # Change the tag and image name below if needed (see https://github.com/docker/metadata-action) + flavor: | + latest=false + tags: | + # set latest tag only for one branch + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', env.LATEST_BRANCH) }},priority=1200 + type=raw,value=${{ inputs.imageTag }},enable=${{ inputs.imageTag != '' }},priority=1100 + type=ref,event=branch + type=ref,event=tag + images: ${{ env.REGISTRY }}/${{ env.REPOSITORY_NAME }}/webapp-functions-aip + - name: "webapp-functions: build and push Docker image" + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_RECORD_UPLOAD: false + with: + context: api + file: webapp_server/webapp-functions.Dockerfile + push: true + tags: ${{ steps.webapp-functions-metadata.outputs.tags }} + labels: ${{ steps.webapp-functions-metadata.outputs.labels }} diff --git a/.gitignore b/.gitignore index 07a2e44c4..8b16b46cc 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ __pycache__ # yalc .yalc yalc.lock + +*storybook.log +storybook-static diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 000000000..ff64d18e3 --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,30 @@ +const config = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + + addons: ['@storybook/preset-create-react-app', '@storybook/addon-docs', '@storybook/addon-onboarding'], + + framework: { + name: '@storybook/react-webpack5', + options: {}, + }, + + docs: { + autodocs: true, + }, + + staticDirs: ['../public'], + + webpackFinal: async (config) => { + config.module.rules = config.module.rules.filter( + (rule) => !(rule.use && rule.use.some((u) => u.loader?.includes('eslint'))) + ); + + config.plugins = config.plugins.filter( + (plugin) => !(plugin.constructor && plugin.constructor.name === 'ESLintWebpackPlugin') + ); + + return config; + }, +}; + +export default config; diff --git a/.storybook/manager.js b/.storybook/manager.js new file mode 100644 index 000000000..9607ec302 --- /dev/null +++ b/.storybook/manager.js @@ -0,0 +1,10 @@ +import { addons } from '@storybook/manager-api'; +import { themes } from '@storybook/theming'; + +addons.setConfig({ + theme: { + ...themes.light, + brandTitle: 'Cosmotech Storybook', + brandImage: '/theme/cosmotech_light_logo.png', + }, +}); diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 000000000..161aabecf --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,27 @@ +// Copyright (c) Cosmo Tech. +// Licensed under the MIT license. +/* eslint-disable react/react-in-jsx-scope */ +import { ThemeProvider, CssBaseline } from '@mui/material'; +import { getTheme } from '../src/theme'; + +const theme = getTheme(false); + +export const decorators = [ + (Story) => ( + + + + + ), +]; + +export default { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; diff --git a/eslint.config.mjs b/eslint.config.mjs index 093e1fa76..9d5c5f178 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -6,6 +6,7 @@ import js from '@eslint/js'; import cypress from 'eslint-plugin-cypress'; import jest from 'eslint-plugin-jest'; import prettier from 'eslint-plugin-prettier'; +import storybook from 'eslint-plugin-storybook'; import globals from 'globals'; import neostandard from 'neostandard'; import path from 'node:path'; @@ -35,10 +36,8 @@ export default [ }, ...neostandardConfig, ...compat.extends('plugin:react/recommended', 'prettier', 'plugin:prettier/recommended', 'plugin:jest/recommended'), - cypress.configs.recommended, cypress.configs.globals, - { plugins: { react, @@ -100,4 +99,5 @@ export default [ 'jest/valid-expect-in-promise': 0, }, }, + ...storybook.configs['flat/recommended'], ]; diff --git a/package.json b/package.json index e35e1dacb..ea4266bfa 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "jwt-decode": "^4.0.0", + "lucide-react": "^0.555.0", "prop-types": "^15.8.1", "react": "^19.1.1", "react-countdown": "^2.3.6", @@ -49,7 +50,9 @@ "eslint": "eslint", "prettier": "npx prettier -w", "link:ui": "yalc add @cosmotech/ui && yalc link @cosmotech/ui && yarn install", - "unlink:ui": "yalc remove @cosmotech/ui && yarn install" + "unlink:ui": "yalc remove @cosmotech/ui && yarn install", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "engines": { "node": "^22", @@ -75,6 +78,17 @@ "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.35.0", + "@storybook/addon-controls": "^9.0.8", + "@storybook/addon-docs": "^10.0.8", + "@storybook/addon-essentials": "^8.6.14", + "@storybook/addon-onboarding": "10.0.8", + "@storybook/addon-storyshots": "7.6.17", + "@storybook/addon-storyshots-puppeteer": "7.6.17", + "@storybook/blocks": "^8.6.14", + "@storybook/manager-api": "^8.6.14", + "@storybook/preset-create-react-app": "^10.0.8", + "@storybook/react-webpack5": "10.0.8", + "@storybook/theming": "^8.6.14", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", @@ -98,6 +112,7 @@ "eslint-plugin-promise": "^7.2.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-storybook": "10.0.8", "globals": "^16.4.0", "http-proxy-middleware": "^3.0.5", "i18next-parser": "^9.3.0", @@ -110,7 +125,10 @@ "react-app-rewired": "^2.2.1", "react-scripts": "^5.0.1", "read-excel-file": "^6.0.1", - "stream-browserify": "^3.0.0" + "sass-loader": "^16.0.6", + "storybook": "10.0.8", + "stream-browserify": "^3.0.0", + "webpack": "5.103.0" }, "packageManager": "yarn@4.9.4" } diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 5acb57b3e..14c2dd2c0 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -95,6 +95,14 @@ } } }, + "tabLayout": { + "statusBar": { + "prerun": { + "message": "This scenario has not been run yet.", + "tooltip": "This scenario has not been run yet." + } + } + }, "cytoviz": { "elementDetails": "Details", "loading": "Loading...", diff --git a/src/AppRoutes.js b/src/AppRoutes.js index a2da24d16..fc5614600 100644 --- a/src/AppRoutes.js +++ b/src/AppRoutes.js @@ -21,13 +21,21 @@ const AppRoutes = () => { <> } /> - + } - /> + > + + + + } + /> + { - + } > diff --git a/src/components/AppBar/AppBar.js b/src/components/AppBar/AppBar.js index b6f5ce463..9ced7e930 100644 --- a/src/components/AppBar/AppBar.js +++ b/src/components/AppBar/AppBar.js @@ -1,33 +1,47 @@ // Copyright (c) Cosmo Tech. // Licensed under the MIT license. +import { Bot, Languages } from 'lucide-react'; import React from 'react'; import PropTypes from 'prop-types'; -import { AppBar as MuiAppBar, Toolbar } from '@mui/material'; -import { HelpMenuWrapper, Logo, ThemeSwitch, UserInfoWrapper, WorkspaceInfo } from './components'; +import { Button, AppBar as MuiAppBar, Toolbar } from '@mui/material'; +import { StatusBar } from '../'; +import { useCurrentSimulationRunner } from '../../state/runner/hooks'; +import { ThemeSwitch } from './components'; export const AppBar = ({ children }) => { + const currentScenario = useCurrentSimulationRunner(); + return ( theme.palette.appbar.main, - color: (theme) => theme.palette.appbar.contrastText, + backgroundColor: (theme) => theme.palette.background.background01.main, + color: (theme) => theme.palette.neutral.neutral02.main, + boxShadow: 'none', + borderBottom: (theme) => `1px solid ${theme.palette.background.background02.main}`, }} > - - -
{children}
+ +
{children}
+ {currentScenario?.data?.name && ( + + )} + - - - +
); }; AppBar.propTypes = { - /** - * React component to be implemented in dynamic part of the app bar - */ children: PropTypes.node, }; diff --git a/src/components/AppBar/components/ThemeSwitch.js b/src/components/AppBar/components/ThemeSwitch.js index 6034d799a..d6f4607f3 100644 --- a/src/components/AppBar/components/ThemeSwitch.js +++ b/src/components/AppBar/components/ThemeSwitch.js @@ -1,30 +1,33 @@ // Copyright (c) Cosmo Tech. // Licensed under the MIT license. +import { Moon, Sun } from 'lucide-react'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Brightness2 as Brightness2Icon, WbSunny as WbSunnyIcon } from '@mui/icons-material'; -import { Fade, IconButton, Tooltip } from '@mui/material'; +import { Button, Fade, Tooltip } from '@mui/material'; import { useApplicationTheme } from '../../../state/app/hooks'; export const ThemeSwitch = () => { const { t } = useTranslation(); const { isDarkTheme, toggleTheme } = useApplicationTheme(); - const { tooltipText, icon } = useMemo( + const { tooltipText } = useMemo( () => ({ tooltipText: isDarkTheme ? t('genericcomponent.switchtheme.light', 'Switch to light') : t('genericcomponent.switchtheme.dark', 'Switch to dark'), - icon: isDarkTheme ? : , }), [t, isDarkTheme] ); return ( - theme.palette.appbar.contrastText }} onClick={toggleTheme} size="large"> - {icon} - + +); + +export const Default = Template.bind({}); +Default.args = { + label: 'Button Text', + variant: 'default', + state: 'enabled', + icon: Star, +}; +Default.parameters = { + docs: { + source: { + code: ``, + }, + }, +}; + +export const Highlighted = Template.bind({}); +Highlighted.args = { + label: 'Button Text', + variant: 'highlighted', + state: 'enabled', + icon: Star, +}; +Highlighted.parameters = { + docs: { + source: { + code: ``, + }, + }, +}; + +export const Copilot = Template.bind({}); +Copilot.args = { + label: 'Button Text', + variant: 'copilot', + state: 'enabled', + icon: Star, +}; +Copilot.parameters = { + docs: { + source: { + code: ``, + }, + }, +}; + +export const OutlinedActive = Template.bind({}); +OutlinedActive.args = { + label: '', + variant: 'outlined', + state: 'active', + icon: Star, +}; +OutlinedActive.parameters = { + docs: { + source: { + code: `