diff --git a/.github/workflows/build-frontend.yml b/.github/workflows/build-frontend.yml index cd8c1cf58..d4f1e1f70 100644 --- a/.github/workflows/build-frontend.yml +++ b/.github/workflows/build-frontend.yml @@ -51,6 +51,8 @@ jobs: NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: build run: npm run build:development + - name: build(library) + run: npm run build:lib test: runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/release-pagoda-core.yml b/.github/workflows/release-pagoda-core.yml new file mode 100644 index 000000000..7988172a8 --- /dev/null +++ b/.github/workflows/release-pagoda-core.yml @@ -0,0 +1,37 @@ +name: Release pagoda-core npm package for custom views to GitHub npm Registry + +on: + push: + tags: + # NOTE it should be pagoda-core-.. + - 'pagoda-core-*' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + registry-url: https://npm.pkg.github.com/ + scope: "@dmm-com" + - name: Get tag name + id: get_tag + run: echo "tag=${GITHUB_REF#refs/tags/pagoda-core-}" >> $GITHUB_OUTPUT + - name: Set version + run: | + npm version "${GITHUB_REF#refs/tags/pagoda-core-}" --no-git-tag-version + - name: Install dependencies + run: npm ci + env: + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Build library + run: npm run build:lib + - name: Publish package + run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.gitignore b/.gitignore index 9316a3d34..a0aaf9e00 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,7 @@ static_root node_modules static/js/ui.js* coverage +frontend/dist # local django configuration airone/settings.py diff --git a/docs/content/getting_started/development.md b/docs/content/getting_started/development.md index de06b6c57..0d23dd14d 100644 --- a/docs/content/getting_started/development.md +++ b/docs/content/getting_started/development.md @@ -264,7 +264,7 @@ user@hostname:~/pagoda$ source virtualenv/bin/activate (virtualenv) user@hostname:~/pagoda$ celery -A airone worker -l info ``` -## [Experimental] Build the new UI with React +## Build the new UI with React `/ui/` serves React-based new UI. Before you try it, you need to build `ui.js`: @@ -366,3 +366,14 @@ When you want to run individual test (e.g. frontend/src/components/user/UserList ``` user@hostname:~/pagoda$ npx jest -u frontend/src/components/user/UserList.test.tsx ``` + +## Release pagoda-core package for custom views + +We publish the `pagoda-core` package to GitHub npm Registry for custom views. +When you want to release a new version of the package, create a tag with the format `pagoda-core-x.y.z` (e.g. `pagoda-core-0.0.1`). The GitHub Actions workflow will automatically build and publish the package. + +If you hope to try building the module: + +```sh +$ npm run build:lib +``` diff --git a/frontend/src/AppRouter.tsx b/frontend/src/AppRouter.tsx deleted file mode 100644 index 9cea60654..000000000 --- a/frontend/src/AppRouter.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React, { FC } from "react"; -import { - createBrowserRouter, - createRoutesFromElements, - Outlet, - Route, - RouterProvider, - useRouteError, -} from "react-router-dom"; - -import { ACLHistoryPage } from "./pages/ACLHistoryPage"; -import { EntryCopyPage } from "./pages/EntryCopyPage"; -import { EntryDetailsPage } from "./pages/EntryDetailsPage"; -import { EntryRestorePage } from "./pages/EntryRestorePage"; -import { NotFoundErrorPage } from "./pages/NotFoundErrorPage"; -import { RoleEditPage } from "./pages/RoleEditPage"; -import { RoleListPage } from "./pages/RoleListPage"; - -import { Header } from "components/common/Header"; -import { ACLEditPage } from "pages/ACLEditPage"; -import { AdvancedSearchPage } from "pages/AdvancedSearchPage"; -import { AdvancedSearchResultsPage } from "pages/AdvancedSearchResultsPage"; -import { DashboardPage } from "pages/DashboardPage"; -import { EntityEditPage } from "pages/EntityEditPage"; -import { EntityHistoryPage } from "pages/EntityHistoryPage"; -import { EntityListPage } from "pages/EntityListPage"; -import { EntryEditPage } from "pages/EntryEditPage"; -import { EntryHistoryListPage } from "pages/EntryHistoryListPage"; -import { EntryListPage } from "pages/EntryListPage"; -import { GroupEditPage } from "pages/GroupEditPage"; -import { GroupListPage } from "pages/GroupListPage"; -import { JobListPage } from "pages/JobListPage"; -import { LoginPage } from "pages/LoginPage"; -import { TriggerEditPage } from "pages/TriggerEditPage"; -import { TriggerListPage } from "pages/TriggerListPage"; -import { UserEditPage } from "pages/UserEditPage"; -import { UserListPage } from "pages/UserListPage"; -import { - aclHistoryPath, - aclPath, - advancedSearchPath, - advancedSearchResultPath, - copyEntryPath, - editEntityPath, - editTriggerPath, - entitiesPath, - entityEntriesPath, - entityHistoryPath, - entryDetailsPath, - entryEditPath, - groupPath, - groupsPath, - jobsPath, - loginPath, - newEntityPath, - newEntryPath, - newGroupPath, - newRolePath, - newTriggerPath, - newUserPath, - restoreEntryPath, - rolePath, - rolesPath, - showEntryHistoryPath, - topPath, - triggersPath, - userPath, - usersPath, -} from "routes/Routes"; - -// re-throw error to be caught by the root error boundary -const ErrorBridge: FC = () => { - throw useRouteError(); -}; - -interface Props { - customRoutes?: { - path: string; - element: React.ReactNode; - }[]; -} - -export const AppRouter: FC = ({ customRoutes }) => { - const router = createBrowserRouter( - createRoutesFromElements( - }> - } /> - -
- - - } - > - {customRoutes && - customRoutes.map((r) => ( - - ))} - - } /> - } - /> - } /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } /> - } - /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - ) - ); - - return ; -}; diff --git a/frontend/src/ErrorHandler.tsx b/frontend/src/ErrorHandler.tsx index 7dc20ed07..46b63352f 100644 --- a/frontend/src/ErrorHandler.tsx +++ b/frontend/src/ErrorHandler.tsx @@ -9,7 +9,6 @@ import { import { styled } from "@mui/material/styles"; import React, { FC, useCallback, useEffect, useState } from "react"; import { ErrorBoundary, FallbackProps } from "react-error-boundary"; -import { useNavigate } from "react-router-dom"; import { useError } from "react-use"; import { ForbiddenErrorPage } from "./pages/ForbiddenErrorPage"; @@ -46,16 +45,15 @@ interface GenericErrorProps { } const GenericError: FC = ({ children }) => { - const navigate = useNavigate(); const [open, setOpen] = useState(true); const handleGoToTop = useCallback(() => { - navigate(topPath(), { replace: true }); - }, [navigate]); + window.location.href = topPath(); + }, []); const handleReload = useCallback(() => { - navigate(0); - }, [navigate]); + window.location.reload(); + }, []); return ( setOpen(false)}> diff --git a/frontend/src/components/acl/index.ts b/frontend/src/components/acl/index.ts new file mode 100644 index 000000000..1b8facd79 --- /dev/null +++ b/frontend/src/components/acl/index.ts @@ -0,0 +1,2 @@ +export * from "./ACLForm"; +export * from "./ACLHistoryList"; diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts new file mode 100644 index 000000000..e729c5815 --- /dev/null +++ b/frontend/src/components/common/index.ts @@ -0,0 +1,15 @@ +export * from "./AironeBreadcrumbs"; +export * from "./AironeModal"; +export * from "./AironeTableHeadCell"; +export * from "./AironeTableHeadRow"; +export * from "./AutocompleteWithAllSelector"; +export * from "./ClipboardCopyButton"; +export * from "./Confirmable"; +export * from "./FlexBox"; +export * from "./ImportForm"; +export * from "./Loading"; +export * from "./PageHeader"; +export * from "./PaginationFooter"; +export * from "./RateLimitedClickable"; +export * from "./SearchBox"; +export * from "./SubmitButton"; diff --git a/frontend/src/components/entity/index.ts b/frontend/src/components/entity/index.ts new file mode 100644 index 000000000..2e12250f0 --- /dev/null +++ b/frontend/src/components/entity/index.ts @@ -0,0 +1,4 @@ +// FIXME rethink export scope + +export * from "./EntityBreadcrumbs"; +export * from "./EntityControlMenu"; diff --git a/frontend/src/components/entry/index.ts b/frontend/src/components/entry/index.ts new file mode 100644 index 000000000..6038db1fb --- /dev/null +++ b/frontend/src/components/entry/index.ts @@ -0,0 +1,13 @@ +// FIXME rethink export scope + +export * from "./AttributeValue"; +export * from "./CopyForm"; +export * from "./EntryAttributes"; +export * from "./EntryBreadcrumbs"; +export * from "./EntryControlMenu"; +export * from "./EntryImportModal"; +export * from "./EntryListCard"; +export * from "./EntryReferral"; + +export * from "./entryForm/EntryFormSchema"; +export * from "./entryForm/AttributeValueField"; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts new file mode 100644 index 000000000..7f7111ce1 --- /dev/null +++ b/frontend/src/components/index.ts @@ -0,0 +1,4 @@ +export * from "./acl"; +export * from "./common"; +export * from "./entity"; +export * from "./entry"; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts new file mode 100644 index 000000000..f6fd9a665 --- /dev/null +++ b/frontend/src/hooks/index.ts @@ -0,0 +1,7 @@ +export * from "./useAsyncWithThrow"; +export * from "./useFormNotification"; +export * from "./usePage"; +export * from "./usePrompt"; +export * from "./useSimpleSearch"; +export * from "./useTranslation"; +export * from "./useTypedParams"; diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts new file mode 100644 index 000000000..5c62e04f5 --- /dev/null +++ b/frontend/src/i18n/index.ts @@ -0,0 +1 @@ +export * from "./config"; diff --git a/frontend/src/index.ts b/frontend/src/index.ts new file mode 100644 index 000000000..97ef9e21b --- /dev/null +++ b/frontend/src/index.ts @@ -0,0 +1,10 @@ +export * from "./AppBase"; + +export * from "./components"; +export * from "./hooks"; +export * from "./i18n"; +export * from "./pages"; +export * from "./repository"; +export * from "./routes"; +export * from "./services"; +export * from "./TestWrapper"; diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts new file mode 100644 index 000000000..43466c7b0 --- /dev/null +++ b/frontend/src/pages/index.ts @@ -0,0 +1,9 @@ +// FIXME rethink export scope + +export * from "./EntryCopyPage"; +export * from "./EntryDetailsPage"; +export * from "./EntryEditPage"; +export * from "./EntryHistoryListPage"; +export * from "./EntryListPage"; + +export * from "./UnavailableErrorPage"; diff --git a/frontend/src/repository/index.ts b/frontend/src/repository/index.ts new file mode 100644 index 000000000..5b950f070 --- /dev/null +++ b/frontend/src/repository/index.ts @@ -0,0 +1,2 @@ +export * from "./AironeApiClient"; +export * from "./LocalStorageUtil"; diff --git a/frontend/src/routes/index.ts b/frontend/src/routes/index.ts new file mode 100644 index 000000000..696c25e2b --- /dev/null +++ b/frontend/src/routes/index.ts @@ -0,0 +1,2 @@ +export * from "./AppRouter"; +export * from "./Routes"; diff --git a/frontend/src/services/index.ts b/frontend/src/services/index.ts new file mode 100644 index 000000000..1639a321b --- /dev/null +++ b/frontend/src/services/index.ts @@ -0,0 +1,7 @@ +export * from "./AironeAPIErrorUtil"; +export * from "./Constants"; +export * from "./DateUtil"; +export * from "./JobUtil"; +export * from "./ServerContext"; +export * from "./StringUtil"; +export * from "./ZodSchemaUtil"; diff --git a/package-lock.json b/package-lock.json index ac7c6d7d3..2dbcecb57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "airone", - "version": "1.0.0", + "name": "@syucream/pagoda-core", + "version": "1.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "airone", - "version": "1.0.0", + "name": "@syucream/pagoda-core", + "version": "1.0.1", "license": "GPLv2", "devDependencies": { "@babel/core": "^7.12.10", @@ -77,6 +77,11 @@ "webpack-bundle-analyzer": "^4.9.0", "webpack-cli": "^4.4.0", "zod": "^3.22.4" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^6.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index 91a6c0d43..74fc61304 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,18 @@ { - "name": "airone", - "version": "1.0.0", + "name": "@dmm-com/pagoda-core", + "version": "1.0.1", "description": "[![CircleCI](https://circleci.com/gh/dmm-com/airone.svg?style=svg&circle-token=2e12c068b0ed1bab9d0c2d72529d5ee1da9b53b4)](https://circleci.com/gh/dmm-com/airone)", - "main": "App.js", + "main": "frontend/dist/pagoda-core.js", + "types": "frontend/dist/index.d.ts", "directories": { "doc": "docs" }, "scripts": { "build": "npm run build:development", - "build:development": "webpack build --mode development", - "build:development:analyze": "webpack build --mode development --analyze", - "build:production": "webpack build --mode production", - "build:production:analyze": "webpack build --mode production --analyze", + "build:development": "webpack build --entry ./frontend/src/App.tsx --mode development", + "build:development:analyze": "webpack build --entry ./frontend/src/App.tsx --mode development --analyze", + "build:production": "webpack build --entry ./frontend/src/App.tsx --mode production", + "build:production:analyze": "webpack build --entry ./frontend/src/App.tsx --mode production --analyze", "build:custom": "npm run build:custom_development", "build:custom_development": "webpack build --entry ./frontend/src/customview/CustomApp.tsx --mode development", "build:custom_production": "webpack build --entry ./frontend/src/customview/CustomApp.tsx --mode production", @@ -20,24 +21,33 @@ "generate:client_docker": "./tools/generate_client.sh apiclient/typescript-fetch/src/autogenerated --docker", "generate:custom_client_docker": "export DJANGO_CONFIGURATION=DRFSpectacularCustomView && ./tools/generate_client.sh frontend/src/apiclient/autogenerated --docker", "link:client": "cd apiclient/typescript-fetch/ && npm link && cd ../../ && npm link '@dmm-com/airone-apiclient-typescript-fetch'", - "watch": "webpack build --watch --mode development", + "watch": "webpack build --entry ./frontend/src/App.tsx --watch --mode development", "watch:custom": "webpack build --watch --entry ./frontend/src/customview/CustomApp.tsx --mode development", "lint": "npx eslint frontend && npx prettier --check frontend", "fix": "npx eslint --fix frontend ; npx prettier --write frontend", "test": "TZ=UTC npx jest --coverage frontend", - "test:update": "TZ=UTC npx jest -u frontend" + "test:update": "TZ=UTC npx jest -u frontend", + "build:lib": "npm run build:lib:code && npm run build:lib:types", + "build:lib:code": "webpack --config webpack.library.config.js --mode development", + "build:lib:types": "tsc --project tsconfig.lib.json" }, "repository": { "type": "git", - "url": "git+https://github.com/userlocalhost/airone.git" + "url": "git+https://github.com/dmm-com/airone.git" }, + "publishConfig": { + "registry": "https://npm.pkg.github.com/" + }, + "files": [ + "frontend/dist" + ], "keywords": [], "author": "Hiroyasu OHYAMA", "license": "GPLv2", "bugs": { - "url": "https://github.com/userlocalhost/airone/issues" + "url": "https://github.com/dmm-com/pagoda/issues" }, - "homepage": "https://github.com/userlocalhost/airone#readme", + "homepage": "https://github.com/dmm-com/pagoda#readme", "devDependencies": { "@babel/core": "^7.12.10", "@babel/plugin-transform-runtime": "^7.14.5", @@ -107,5 +117,10 @@ "webpack-bundle-analyzer": "^4.9.0", "webpack-cli": "^4.4.0", "zod": "^3.22.4" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^6.0.0" } } diff --git a/tsconfig.lib.json b/tsconfig.lib.json new file mode 100644 index 000000000..3c01b4a5c --- /dev/null +++ b/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "frontend/dist", + "declaration": true, + "declarationDir": "frontend/dist", + "emitDeclarationOnly": true, + "isolatedModules": true, + "skipLibCheck": true + }, + "include": ["frontend/src/**/*"], + "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/webpack.library.config.js b/webpack.library.config.js new file mode 100644 index 000000000..24b8bc933 --- /dev/null +++ b/webpack.library.config.js @@ -0,0 +1,43 @@ +const path = require('path'); + +module.exports = { + mode: 'production', + entry: './frontend/src/index.ts', + output: { + path: path.resolve(__dirname, 'frontend/dist'), + filename: 'pagoda-core.js', + library: { + name: 'Pagoda Core Module', + type: 'umd', + }, + globalObject: 'this', + }, + module: { + rules: [ + { + test: /\.tsx?$/, + exclude: /node_modules/, + loader: 'ts-loader', + options: { + transpileOnly: true, + } + }, + { + test: /\.ts$/, + include: /node_modules\/@dmm-com\/airone-apiclient-typescript-fetch/, + loader: 'ts-loader', + options: { + transpileOnly: true, + } + }, + ], + }, + resolve: { + extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"], + modules: [ + path.resolve(__dirname, 'frontend/src'), + 'node_modules', + ], + }, + externals: ['react', 'react-dom', 'react-router-dom'], +} \ No newline at end of file