Platform UI - React integration (#5011)

* Add SPA views for react #2789

* split up frontend urls

* Add settings for frontend url loading

* add new UI scaffold

* remove tracking insert

* add platform app

* ensure static indexes work too

* add lingui

* add lingui config

* add mgmt tasks

* add base locales

* settings for frontend dev

* fix typo

* update deps

* add pre-commit

* add eslint

* add testing scaffold

* fix paths

* remove error - tests trip correctly

* merge workflow

* cleanup samples

* use name inline with other tests

* Add real worl frontend tests

* setup env

* tun migrations first

* optimize setup time

* setup demo dataset

* optimize run setup

* add test for class ui

* rename

* fix typo

* and another typo

* do install

* run migrations first

* fix name

* cleanup

* use other credentials

* use other credentials

* fix qc

* move envs to qc

* remove create_site

* reduce testing env

* fix test

* fix test call

* allaccess user

* add ui plattform check

* add better check

* remove unneeded env

* enable debug

* reduce wait time

* also build frontend on static

* add sort plugin

* fix order

* run pre-commit fixes

* add node min version

* Docker container (#129)

* Fix allocation check for completing build order (#5199)

- Allocation check only applies to untracked line items

* docker dev

Install required node packages to docker development image

* add import order settings

* cleanout built ui

* remove default arg from build

* remove eslint

* optimize svg

* add build step for plattform UI

* fix install command

* use alpine commands

* do not use cache when creating image

---------

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
Matthias Mair 2023-07-18 14:45:49 +02:00 committed by GitHub
parent b717011f06
commit 3e37469350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 2817 additions and 7 deletions

View File

@ -7,7 +7,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
ARG WORKSPACE="/workspaces/InvenTree"
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
ARG NODE_VERSION="18"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.

View File

@ -16,6 +16,7 @@ python3 -m venv dev/venv
pip install invoke
invoke update
invoke setup-dev
invoke frontend-install
# remove existing gitconfig created by "Avoiding Dubious Ownership" step
# so that it gets copied from host to the container to have your global

View File

@ -29,6 +29,7 @@ jobs:
outputs:
server: ${{ steps.filter.outputs.server }}
migrations: ${{ steps.filter.outputs.migrations }}
frontend: ${{ steps.filter.outputs.frontend }}
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
@ -43,6 +44,8 @@ jobs:
migrations:
- '**/migrations/**'
- '.github/workflows**'
frontend:
- 'src/frontend/**'
pep_style:
name: Style [Python]
@ -387,3 +390,38 @@ jobs:
cp test-db/stable_0.12.0.sqlite3 /home/runner/work/InvenTree/db.sqlite3
chmod +rw /home/runner/work/InvenTree/db.sqlite3
invoke migrate
plattform_ui:
name: Tests - UI Platform
runs-on: ubuntu-20.04
timeout-minutes: 60
needs: paths-filter
if: needs.paths-filter.outputs.frontend == 'true'
env:
INVENTREE_DB_ENGINE: sqlite3
INVENTREE_DB_NAME: /home/runner/work/InvenTree/db.sqlite3
INVENTREE_DEBUG: True
INVENTREE_PLUGINS_ENABLED: false
steps:
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # pin@v3.1.0
- name: Environment Setup
uses: ./.github/actions/setup
with:
npm: true
install: true
update: true
- name: Set up test data
run: invoke setup-test -i
- name: Install dependencies
run: cd src/frontend && yarn install
- name: Install Playwright Browsers
run: cd src/frontend && npx playwright install --with-deps
- name: Run Playwright tests
run: cd src/frontend && npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: src/frontend/playwright-report/
retention-days: 30

View File

@ -54,3 +54,23 @@ repos:
docs/docs/javascripts/.*|
docs/docs/webfonts/.*
)$
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v3.0.0-alpha.9-for-vscode"
hooks:
- id: prettier
files: ^src/frontend/.*\.(js|jsx|ts|tsx)$
additional_dependencies:
- "prettier@^2.4.1"
- "@trivago/prettier-plugin-sort-imports"
- repo: https://github.com/pre-commit/mirrors-eslint
rev: "v8.42.0"
hooks:
- id: eslint
additional_dependencies:
- eslint@^8.41.0
- eslint-config-google@^0.14.0
- eslint-plugin-react@6.10.3
- babel-eslint@6.1.2
- "@typescript-eslint/eslint-plugin@latest"
- "@typescript-eslint/parser"
files: ^src/frontend/.*\.(js|jsx|ts|tsx)$

View File

@ -82,10 +82,11 @@ The HEAD of the "stable" branch represents the latest stable release code.
## Environment
### Target version
We are currently targeting:
| Name | Minimum version |
|---|---|
| Python | 3.9 |
| Django | 3.2 |
| Name | Minimum version | Note |
|---|---| --- |
| Python | 3.9 | |
| Django | 3.2 | |
| Node | 18 | Only needed for frontend development |
### Auto creating updates
The following tools can be used to auto-upgrade syntax that was depreciated in new versions:

View File

@ -98,6 +98,15 @@ RUN chmod +x init.sh
ENTRYPOINT ["/bin/sh", "./init.sh"]
# Frontend builder image:
FROM inventree_base as frontend
RUN apk add --no-cache --update nodejs npm && npm install -g yarn
COPY InvenTree ${INVENTREE_HOME}/InvenTree
COPY src ${INVENTREE_HOME}/src
COPY tasks.py ${INVENTREE_HOME}/tasks.py
RUN cd ${INVENTREE_HOME}/InvenTree && inv frontend-compile
# InvenTree production image:
# - Copies required files from local directory
# - Starts a gunicorn webserver
@ -111,6 +120,7 @@ ENV INVENTREE_COMMIT_DATE="${commit_date}"
# Copy source code
COPY InvenTree ./InvenTree
COPY --from=frontend ${INVENTREE_HOME}/InvenTree/web/static/web ./InvenTree/web/static/web
# Launch the production server
# TODO: Work out why environment variables cannot be interpolated in this command
@ -120,6 +130,9 @@ CMD gunicorn -c ./gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8000 --chdir ./Inve
FROM inventree_base as dev
# Install nodejs / npm / yarn
RUN apk add --no-cache --update nodejs npm && npm cache clean -f && npm install -g n && n stable && npm install -g yarn
# The development image requires the source code to be mounted to /home/inventree/
# So from here, we don't actually "do" anything, apart from some file management

View File

@ -33,7 +33,7 @@ from . import config
INVENTREE_NEWS_URL = 'https://inventree.org/news/feed.atom'
# Determine if we are running in "test" mode e.g. "manage.py test"
TESTING = 'test' in sys.argv
TESTING = 'test' in sys.argv or 'TESTING' in os.environ
if TESTING:
@ -76,6 +76,9 @@ if version_file.exists():
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = get_boolean_setting('INVENTREE_DEBUG', 'debug', True)
ENABLE_CLASSIC_FRONTEND = get_boolean_setting('INVENTREE_CLASSIC_FRONTEND', 'classic_frontend', True)
ENABLE_PLATFORM_FRONTEND = get_boolean_setting('INVENTREE_PLATFORM_FRONTEND', 'platform_frontend', True)
# Configure logging settings
log_level = get_setting('INVENTREE_LOG_LEVEL', 'log_level', 'WARNING')
@ -203,6 +206,7 @@ INSTALLED_APPS = [
'stock.apps.StockConfig',
'users.apps.UsersConfig',
'plugin.apps.PluginAppConfig',
'web',
'generic',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last

View File

@ -33,6 +33,7 @@ from report.api import report_api_urls
from stock.api import stock_api_urls
from stock.urls import stock_urls
from users.api import user_urls
from web.urls import urlpatterns as platform_urls
from .api import APISearchView, InfoView, NotFoundView
from .magic_login import GetSimpleLoginView
@ -162,7 +163,7 @@ backendpatterns = [
re_path(r'^api-doc/', SpectacularRedocView.as_view(url_name='schema'), name='api-doc'),
]
frontendpatterns = [
classic_frontendpatterns = [
# Apps
re_path(r'^build/', include(build_urls)),
@ -205,6 +206,21 @@ frontendpatterns = [
re_path(r'^accounts/', include('allauth.urls')), # included urlpatterns
]
new_frontendpatterns = [
# Platform urls
re_path(r'^platform/', include(platform_urls)),
]
# Load patterns for frontend according to settings
frontendpatterns = []
if settings.ENABLE_CLASSIC_FRONTEND:
frontendpatterns.append(re_path('', include(classic_frontendpatterns)))
if settings.ENABLE_PLATFORM_FRONTEND:
frontendpatterns.append(re_path('', include(new_frontendpatterns)))
# Append custom plugin URLs (if plugin support is enabled)
if settings.PLUGINS_ENABLED:
frontendpatterns.append(get_plugin_urls())

View File

@ -58,6 +58,12 @@ database:
# Use the environment variable INVENTREE_DEBUG
debug: True
# Set enabled frontends
# Use the environment variable INVENTREE_CLASSIC_FRONTEND
# classic_frontend: True
# Use the environment variable INVENTREE_PLATFORM_FRONTEND
# platform_frontend: True
# Configure the system logging level
# Use environment variable INVENTREE_LOG_LEVEL
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL

View File

@ -0,0 +1,18 @@
{% load spa_helper %}
{% load inventree_extras %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% inventree_instance_name %}</title>
</head>
<body>
<div id="root"></div>
{% spa_bundle %}
</body>
</html>

View File

@ -0,0 +1,36 @@
"""Template tag to render SPA imports."""
import json
from logging import getLogger
from pathlib import Path
from django import template
from django.conf import settings
from django.utils.safestring import mark_safe
logger = getLogger("gwaesser_backend")
register = template.Library()
@register.simple_tag
def spa_bundle():
"""Render SPA bundle."""
manifest = Path(__file__).parent.parent.joinpath("static/web/manifest.json")
if not manifest.exists():
logger.error("Manifest file not found")
return
manifest_data = json.load(manifest.open())
index = manifest_data.get("index.html")
dynmanic_files = index.get("dynamicImports", [])
imports_files = "".join(
[
f'<script type="module" src="{settings.STATIC_URL}web/{manifest_data[file]["file"]}"></script>'
for file in dynmanic_files
]
)
return mark_safe(
f"""<script type="module" src="{settings.STATIC_URL}web/{index['file']}"></script>{imports_files}"""
)

26
InvenTree/web/urls.py Normal file
View File

@ -0,0 +1,26 @@
"""URLs for web app."""
from django.conf import settings
from django.shortcuts import redirect
from django.urls import path, re_path
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import TemplateView
class RedirectAssetView(TemplateView):
"""View to redirect to static asset."""
def get(self, request, *args, **kwargs):
"""Redirect to static asset."""
return redirect(
f"{settings.STATIC_URL}web/assets/{kwargs['path']}", permanent=True
)
spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name="web/index.html"))
urlpatterns = [
path("assets/<path:path>", RedirectAssetView.as_view()),
re_path(r"^(?P<path>.*)/$", spa_view),
path("", spa_view),
]

8
src/frontend/.babelrc Normal file
View File

@ -0,0 +1,8 @@
{
"presets": [
"@babel/preset-react"
],
"plugins": [
"macros"
]
}

View File

@ -0,0 +1,7 @@
/* eslint-env node */
module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
root: true,
};

29
src/frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Testing
/test-results/
/playwright-report/
/playwright/.cache/

19
src/frontend/.linguirc Normal file
View File

@ -0,0 +1,19 @@
{
"locales": ["en", "de", "hu", "pseudo-LOCALE"],
"catalogs": [{
"path": "src/locales/{locale}/messages",
"include": ["src"],
"exclude": ["**/node_modules/**"]
}],
"format": "po",
"orderBy": "origin",
"sourceLocale": "en",
"pseudoLocale": "pseudo-LOCALE",
"fallbackLocales": {
"default": "en",
"pseudo-LOCALE": "en"
},
"extractBabelOptions": {
"presets": ["@babel/preset-typescript"]
}
}

9
src/frontend/.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 80,
"importOrder": ["<THIRD_PARTY_MODULES>", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}

16
src/frontend/index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/inventree.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>InvenTree</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

36
src/frontend/package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "InvenTreeUI",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"extract": "lingui extract",
"compile": "lingui compile --typescript"
},
"dependencies": {
"@lingui/core": "^4.2.1",
"@lingui/react": "^4.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.20.5",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@lingui/cli": "^4.2.1",
"@lingui/macro": "^4.2.1",
"@playwright/test": "^1.35.0",
"@types/node": "^20.3.0",
"@types/react": "^18.0.24",
"@types/react-dom": "^18.0.8",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.0.0",
"babel-plugin-macros": "^3.1.0",
"typescript": "^5.1.3",
"vite": "^4.0.3",
"vite-plugin-babel-macros": "^1.0.6"
}
}

View File

@ -0,0 +1,40 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: 'html',
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome']
}
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox']
}
}
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'invoke server -a 127.0.0.1:8000',
url: 'http://127.0.0.1:8000/api/',
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
timeout: 120 * 1000
},
use: {
baseURL: 'http://127.0.0.1:8000',
trace: 'on-first-retry'
}
});

View File

@ -0,0 +1,291 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="inventree.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
version="1.1"
id="svg2"
viewBox="0 0 400 400.00001"
height="400"
width="400"
inkscape:export-filename="C:\inventree\images\logo\inventree.png"
inkscape:export-xdpi="499.92001"
inkscape:export-ydpi="499.92001">
<sodipodi:namedview
inkscape:window-maximized="1"
inkscape:window-y="-8"
inkscape:window-x="-8"
inkscape:window-height="1017"
inkscape:window-width="1920"
showgrid="false"
inkscape:current-layer="g2907"
inkscape:document-units="px"
inkscape:cy="426.62472"
inkscape:cx="-960.58794"
inkscape:zoom="0.31819805"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
units="px" />
<defs
id="defs4">
<inkscape:path-effect
effect="skeletal"
id="path-effect4158"
is_visible="true"
pattern="m 446.48742,503.05596 1,0"
copytype="single_stretched"
prop_scale="1"
scale_y_rel="false"
spacing="0"
normal_offset="0"
tang_offset="0"
prop_units="false"
vertical_pattern="false"
fuse_tolerance="0"
pattern-nodetypes="cc" />
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-652.36216)">
<g
id="g2907"
transform="matrix(1.7132796,0,0,1.7132796,460.05136,236.12365)">
<g
id="g5713">
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path4896"
d="m -151.78571,359.62883 v 112.76373 l 97.068507,-56.04253 V 303.14815 Z"
style="fill:#ddbc91;fill-opacity:1;stroke:#000000;stroke-width:1.5622561;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:#d9b383;fill-opacity:1;stroke:#000000;stroke-width:1.5622561;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -151.78571,359.62883 v 112.76373 l -97.0685,-56.04253 V 303.14815 Z"
id="path4898"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path4900"
d="m -54.717203,303.14815 -97.068507,56.48068 -97.0685,-56.48068 97.0685,-56.17354 z"
style="fill:#ddbc91;fill-opacity:1;stroke:#000000;stroke-width:1.5622561;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<g
id="g4916"
transform="matrix(1.4761476,0,0,1.4761476,-218.87903,128.54472)"
style="opacity:1;vector-effect:none;fill:#f3f4e4;fill-opacity:1;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1">
<path
id="path4910"
transform="matrix(0.40703466,0,0,0.40703466,-35.955294,75.175547)"
d="M 78.833984,129.19141 V 270.49609 L 200,340.45117 321.16602,270.49609 V 129.19141 L 200,199.69336 Z"
style="vector-effect:none;fill:#f3f4e4;fill-opacity:1;stroke:#000000;stroke-width:2.600106;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
style="vector-effect:none;fill:#f3f4e4;fill-opacity:1;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 94.770135,127.76116 45.451638,156.45782 -3.8668589,127.76116 45.451638,99.220548 Z"
id="path4914"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
</g>
<path
style="opacity:1;vector-effect:none;fill:#d9dbbc;fill-opacity:1;stroke:#000000;stroke-width:1.56225622;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m -151.7857,275.0089 v 84.49065 l -72.80137,42.54091 v -84.90142 z"
id="path4918"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path4920"
d="m -151.7857,275.0089 v 84.49065 l 72.801373,42.54091 v -84.90142 z"
style="opacity:1;vector-effect:none;fill:#eaeccf;fill-opacity:1;stroke:#000000;stroke-width:1.56225622;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<g
style="opacity:1;vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.58749998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
transform="matrix(0.9840984,0,0,0.9840984,-196.5146,205.65597)"
id="g4928">
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path4922"
d="m 45.451638,156.45782 v 57.29292 l 49.318497,-28.47405 v -57.51553 z"
style="vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.58749998;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
style="vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.58749998;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 45.451638,156.45782 v 57.29292 L -3.866859,185.27669 v -57.51553 z"
id="path4924"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path4926"
d="M 94.770135,127.76116 45.451638,156.45782 -3.8668589,127.76116 45.451638,99.220548 Z"
style="vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.58749998;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
<g
transform="translate(0,102.8232)"
id="g5856"
inkscape:export-xdpi="499.92001"
inkscape:export-ydpi="499.92001">
<path
style="opacity:1;vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.7510277;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m -587.59709,399.25327 v 126.38928 l 108.79755,-62.8143 V 335.94789 Z"
id="path5850"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path5852"
d="m -587.59709,399.25327 v 126.38928 l -108.79754,-62.8143 V 335.94789 Z"
style="opacity:1;vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.7510277;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
style="opacity:1;vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.7510277;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m -478.79954,335.94789 -108.79755,63.30538 -108.79754,-63.30538 108.79754,-62.96113 z"
id="path5854"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
</g>
<g
id="g5848"
transform="translate(0,-23.56608)"
inkscape:export-xdpi="499.92001"
inkscape:export-ydpi="499.92001">
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path5803"
d="m -587.59709,399.25327 v 126.38928 l 108.79755,-62.8143 V 335.94789 Z"
style="opacity:1;vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.7510277;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
style="opacity:1;vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.7510277;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m -587.59709,399.25327 v 126.38928 l -108.79754,-62.8143 V 335.94789 Z"
id="path5805"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path5807"
d="m -478.79954,335.94789 -108.79755,63.30538 -108.79754,-63.30538 108.79754,-62.96113 z"
style="opacity:1;vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.7510277;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path4649"
d="m -587.59709,249.29791 v 126.38928 l 108.79755,-62.8143 V 185.99253 Z"
style="fill:#ddbc91;fill-opacity:1;stroke:#000000;stroke-width:1.75102758;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-xdpi="499.92001"
inkscape:export-ydpi="499.92001" />
<path
style="opacity:1;vector-effect:none;fill:#d9b383;fill-opacity:1;stroke:#000000;stroke-width:1.5622561;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m -587.59709,249.29791 v 126.38928 l -108.79754,-62.8143 V 185.99253 Z"
id="path4651"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:export-xdpi="499.92001"
inkscape:export-ydpi="499.92001" />
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path4653"
d="m -478.79954,185.99253 -108.79755,63.30538 -108.79754,-63.30538 108.79754,-62.96113 z"
style="fill:#ddbc91;fill-opacity:1;stroke:#000000;stroke-width:1.75102758;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-xdpi="499.92001"
inkscape:export-ydpi="499.92001" />
<g
id="g4703"
transform="matrix(1.6545144,0,0,1.6545144,-662.79747,-9.7087)"
style="opacity:1;vector-effect:none;fill:#f3f4e4;fill-opacity:1;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:export-xdpi="499.92001"
inkscape:export-ydpi="499.92001">
<path
inkscape:connector-curvature="0"
style="vector-effect:none;fill:#f3f4e4;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m -713.3418,130.54492 v 163.03516 l 139.79883,80.71484 139.80078,-80.71484 V 130.54492 l -139.80078,81.3457 z"
transform="matrix(0.35277777,0,0,0.35277777,247.78456,81.707826)"
id="path4697" />
<path
style="vector-effect:none;fill:#f3f4e4;fill-opacity:1;stroke:#000000;stroke-width:1.05833328;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 94.770135,127.76116 45.451638,156.45782 -3.8668589,127.76116 45.451638,99.220548 Z"
id="path4701"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
</g>
<path
style="opacity:1;vector-effect:none;fill:#d9dbbc;fill-opacity:1;stroke:#000000;stroke-width:1.7510277;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m -587.59709,154.45314 v 94.69988 l -81.59816,47.68125 v -95.1603 z"
id="path4788"
inkscape:connector-curvature="0"
inkscape:export-xdpi="499.92001"
inkscape:export-ydpi="499.92001" />
<path
inkscape:connector-curvature="0"
id="path4830"
d="m -587.59709,154.45314 v 94.69988 l 81.59817,47.68125 v -95.1603 z"
style="opacity:1;vector-effect:none;fill:#eaeccf;fill-opacity:1;stroke:#000000;stroke-width:1.7510277;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:export-xdpi="499.92001"
inkscape:export-ydpi="499.92001" />
<g
style="opacity:1;vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.58749998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
transform="matrix(1.1030096,0,0,1.1030096,-637.73068,76.7201)"
id="g4695"
inkscape:export-xdpi="499.92001"
inkscape:export-ydpi="499.92001">
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path4689"
d="m 45.451638,156.45782 v 57.29292 l 49.318497,-28.47405 v -57.51553 z"
style="vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.58749998;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
style="vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.58749998;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 45.451638,156.45782 v 57.29292 L -3.866859,185.27669 v -57.51553 z"
id="path4691"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
sodipodi:nodetypes="ccccc"
inkscape:connector-curvature="0"
id="path4693"
d="M 94.770135,127.76116 45.451638,156.45782 -3.8668589,127.76116 45.451638,99.220548 Z"
style="vector-effect:none;fill:#90a8d8;fill-opacity:1;stroke:#000000;stroke-width:1.58749998;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
<rect
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:none;stroke-width:1.41293824;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5777"
width="281.65912"
height="552.58508"
x="-728.42657"
y="95.064545"
inkscape:export-xdpi="499.92001"
inkscape:export-ydpi="499.92001" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

10
src/frontend/src/App.tsx Normal file
View File

@ -0,0 +1,10 @@
// Main App
export default function App() {
// Main App component
return (
<>
<h1>Welcome to the new frontend!</h1>
<p>This is a placeholder site</p>
</>
);
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.676"><path fill="#ddbc91" d="M200 199.906v193.196l166.305-96.016V103.139Z"/><path fill="#d9b383" d="M200 199.906v193.196L33.695 297.086V103.139Z"/><path fill="#ddbc91" d="M366.305 103.139 200 199.906 33.695 103.14 200 6.899z"/><g fill="#f3f4e4"><path d="M75.27 127.109v145.46L200 344.583l124.73-72.012V127.109L200 199.684Z"/><path d="M324.73 127.11 200 199.684 75.27 127.109 200 54.93Z"/></g><path fill="#d9dbbc" d="M200 54.929v144.756L75.27 272.569V127.11z"/><path fill="#eaeccf" d="M200 54.929v144.756l124.73 72.884V127.11z"/><path fill="#90a8d8" d="M200 199.901V296.5l83.153-48.008v-96.973zm0 0V296.5l-83.153-48.008v-96.973zm83.153-48.383L200 199.9l-83.153-48.383L200 103.398Z"/></g><g fill="#90a8d8" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"><path d="M-546.667 443.959v216.54l186.4-107.618V335.499Zm0 0v216.54l-186.4-107.618V335.499Z"/><path d="m-360.266 335.499-186.4 108.46-186.401-108.46 186.4-107.87z"/></g><g fill="#90a8d8" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"><path d="M-546.667 227.419v216.54l186.4-107.619V118.96Zm0 0v216.54l-186.4-107.619V118.96Z"/><path d="m-360.266 118.959-186.4 108.46-186.401-108.46 186.4-107.87z"/></g><path fill="#ddbc91" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M-546.667 10.879v216.54l186.4-107.619V-97.58Z"/><path fill="#d9b383" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.676" d="M-546.667 10.879v216.54l-186.4-107.619V-97.58Z"/><path fill="#ddbc91" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="m-360.266-97.581-186.4 108.46-186.401-108.46 186.4-107.87z"/><g fill="#f3f4e4" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.058"><path stroke-width="3" d="M-686.466-70.715V92.321l139.798 80.714 139.801-80.714V-70.715l-139.8 81.346z"/><path stroke-width="2.999" d="m-406.866-70.715-139.8 81.345-139.801-81.345 139.8-80.902Z"/></g><path fill="#d9dbbc" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M-546.667-151.617V10.63l-139.8 81.692V-70.715z"/><path fill="#eaeccf" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M-546.667-151.617V10.63l139.8 81.692V-70.715z"/><g fill="#90a8d8" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.999"><path d="M-546.667 10.873v108.27l93.2-53.81v-108.69zm0 0v108.27l-93.2-53.81v-108.69z"/><path d="m-453.466-43.357-93.2 54.23-93.201-54.23 93.2-53.935Z"/></g><path fill="none" d="M-787.948-253.366h482.56v946.733h-482.56z"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,4 @@
import { Messages } from '@lingui/core';
declare const messages: Messages;
export { messages };

View File

@ -0,0 +1,14 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2023-06-09 22:10+0200\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: de\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"

View File

@ -0,0 +1 @@
/*eslint-disable*/ export const messages = JSON.parse('{}');

View File

@ -0,0 +1,4 @@
import { Messages } from '@lingui/core';
declare const messages: Messages;
export { messages };

View File

@ -0,0 +1,14 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2023-06-09 22:10+0200\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: en\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"

View File

@ -0,0 +1 @@
/*eslint-disable*/ export const messages = JSON.parse('{}');

View File

@ -0,0 +1,4 @@
import { Messages } from '@lingui/core';
declare const messages: Messages;
export { messages };

View File

@ -0,0 +1,14 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2023-06-09 22:10+0200\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: hu\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"

View File

@ -0,0 +1 @@
/*eslint-disable*/ export const messages = JSON.parse('{}');

View File

@ -0,0 +1,4 @@
import { Messages } from '@lingui/core';
declare const messages: Messages;
export { messages };

View File

@ -0,0 +1,14 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2023-06-09 22:10+0200\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: pseudo-LOCALE\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"

View File

@ -0,0 +1 @@
/*eslint-disable*/ export const messages = JSON.parse('{}');

10
src/frontend/src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

1
src/frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,31 @@
import { expect, test } from '@playwright/test';
import exp from 'constants';
test('Check classic index site', async ({ page }) => {
await page.goto('./api/');
await page.goto('./index/');
await expect(page).toHaveTitle('InvenTree Demo Server | Sign In');
await expect(
page.getByRole('heading', { name: 'InvenTree Demo Server' })
).toBeVisible();
await page.getByLabel('username').fill('allaccess');
await page.getByLabel('password').fill('nolimits');
await page.click('button', { text: 'Sign In' });
await page.waitForURL('**/index/');
await page.waitForLoadState('networkidle');
await expect(page).toHaveTitle('InvenTree Demo Server | Index');
await expect(page.getByRole('button', { name: 'allaccess' })).toBeVisible();
await expect(
page.getByRole('link', { name: 'Parts', exact: true })
).toBeVisible();
await expect(
page.getByRole('link', { name: 'Stock', exact: true })
).toBeVisible();
await expect(
page.getByRole('link', { name: 'Build', exact: true })
).toBeVisible();
await expect(page.getByRole('button', { name: 'Buy' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Sell' })).toBeVisible();
});

View File

@ -0,0 +1,20 @@
import { expect, test } from '@playwright/test';
test('Basic Platform UI test', async ({ page }) => {
await page.goto('./index/');
await expect(page).toHaveTitle('InvenTree Demo Server | Sign In');
await expect(
page.getByRole('heading', { name: 'InvenTree Demo Server' })
).toBeVisible();
await page.getByLabel('username').fill('allaccess');
await page.getByLabel('password').fill('nolimits');
await page.click('button', { text: 'Sign In' });
await page.goto('./platform/');
await expect(page).toHaveTitle('InvenTree Demo Server');
await expect(
page.getByRole('heading', { name: 'Welcome to the new frontend!' })
).toBeVisible();
});

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,17 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['macros']
}
})
],
build: {
manifest: true,
outDir: '../../InvenTree/web/static/web'
}
});

1916
src/frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -89,6 +89,19 @@ def manage(c, cmd, pty: bool = False):
), pty=pty)
def yarn(c, cmd, pty: bool = False):
"""Runs a given command against the yarn package manager.
Args:
c: Command line context.
cmd: Yarn command to run.
pty (bool, optional): Run an interactive session. Defaults to False.
"""
path = managePyDir().parent.joinpath('src').joinpath('frontend')
c.run(f'cd "{path}" && {cmd}', pty=pty)
def check_file_existance(filename: str, overwrite: bool = False):
"""Checks if a file exists and asks the user if it should be overwritten.
@ -188,6 +201,7 @@ def remove_mfa(c, mail=''):
def static(c):
"""Copies required static files to the STATIC_ROOT directory, as per Django requirements."""
manage(c, "prerender")
frontend_build(c)
manage(c, "collectstatic --no-input")
@ -290,6 +304,9 @@ def update(c, skip_backup=False):
# Perform database migrations
migrate(c)
# Compile frontend
frontend_compile(c)
# Data tasks
@task(help={
@ -697,3 +714,50 @@ You are probably running the package installer / single-line installer. Please m
Use '--list' for a list of available commands
Use '--help' for help on a specific command""")
@task
def frontend_compile(c):
"""Generate react frontend.
Args:
c: Context variable
"""
frontend_install(c)
frontend_trans(c)
frontend_build(c)
@task
def frontend_install(c):
"""Install frontend requirements.
Args:
c: Context variable
"""
print("Installing frontend dependencies")
yarn(c, "yarn install")
@task
def frontend_trans(c):
"""Compile frontend translations.
Args:
c: Context variable
"""
print("Compiling frontend translations")
yarn(c, "yarn run extract")
yarn(c, "yarn run compile")
@task
def frontend_build(c):
"""Build frontend.
Args:
c: Context variable
"""
print("Building frontend")
yarn(c, "yarn run build --emptyOutDir")