diff --git a/apps/portal/app-config.yaml b/apps/portal/app-config.yaml index 1e07132..f3c2c2b 100644 --- a/apps/portal/app-config.yaml +++ b/apps/portal/app-config.yaml @@ -31,7 +31,8 @@ backend: # The production database configuration is stored in app-config.production.yaml database: client: better-sqlite3 - connection: ':memory:' + connection: + filename: ':memory:' # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir integrations: @@ -132,6 +133,12 @@ catalog: rules: - allow: [Template] + # Multi-Option React Scaffolder Template + - type: file + target: ../../examples/multi-option-react-template/template.yaml + rules: + - allow: [Template] + # NestJS + Prisma Template - type: file target: ../../examples/nestjs-prisma-template/template.yaml diff --git a/apps/portal/examples/multi-option-react-template/content/gitops/argocd-app.yaml b/apps/portal/examples/multi-option-react-template/content/gitops/argocd-app.yaml new file mode 100644 index 0000000..16ef386 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/gitops/argocd-app.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: ${{ values.repoName }}-argocd + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: ${{ values.gitopsRepo }} + targetRevision: HEAD + path: ./ + destination: + server: https://kubernetes.default.svc + namespace: default + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/apps/portal/examples/multi-option-react-template/content/gitops/helios-app.yaml b/apps/portal/examples/multi-option-react-template/content/gitops/helios-app.yaml new file mode 100644 index 0000000..9885625 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/gitops/helios-app.yaml @@ -0,0 +1,28 @@ +apiVersion: app.helios.io/v1alpha1 +kind: HeliosApp +metadata: + name: ${{ values.repoName }} + namespace: default +spec: + owner: ${{ values.owner }} + description: "React application: ${{ values.name }}" + gitRepo: ${{ values.sourceRepo }} + gitBranch: main + imageRepo: ${{ values.image }} + gitopsRepo: ${{ values.gitopsRepo }} + gitopsPath: ${{ values.repoName }} + pipelineName: from-code-to-cluster + webhookSecret: git-credentials-${{ values.repoName }} + port: ${{ values.port }} + testCommand: ${{ values.testCommand }} + components: + - name: ${{ values.repoName }} + type: web-service + properties: + image: ${{ values.image }}:latest + port: ${{ values.port }} + replicas: 1 + traits: + - type: service + properties: + port: ${{ values.port }} diff --git a/apps/portal/examples/multi-option-react-template/content/gitops/pipeline.yaml b/apps/portal/examples/multi-option-react-template/content/gitops/pipeline.yaml new file mode 100644 index 0000000..8bbfe9a --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/gitops/pipeline.yaml @@ -0,0 +1,40 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + name: ${{ values.repoName }}-pipeline-run + annotations: + janus-idp.io/tekton: ${{ values.repoName }} +spec: + serviceAccountName: pipeline + pipelineRef: + name: from-code-to-cluster + params: + - name: image-repo + value: ${{ values.image }} + - name: app-repo-url + value: ${{ values.sourceRepo }} + - name: app-repo-revision + value: main + - name: GITOPS_REPO_URL + value: ${{ values.gitopsRepo }} + - name: GITOPS_REPO_BRANCH + value: main + - name: MANIFEST_PATH + value: ./helios-app.yaml + - name: docker-secret + value: docker-credentials + - name: test-command + value: ${{ values.testCommand }} + - name: argocd-namespace + value: argocd + - name: argocd-app-name + value: ${{ values.repoName }}-argocd + workspaces: + - name: source-workspace + volumeClaimTemplate: + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/apps/portal/examples/multi-option-react-template/content/gitops/triggers.yaml b/apps/portal/examples/multi-option-react-template/content/gitops/triggers.yaml new file mode 100644 index 0000000..1fec45c --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/gitops/triggers.yaml @@ -0,0 +1,107 @@ +apiVersion: triggers.tekton.dev/v1beta1 +kind: TriggerBinding +metadata: + name: ${{ values.repoName }}-git-binding +spec: + params: + - name: git-repo-url + value: $(body.repository.clone_url) + - name: git-revision + value: $(body.head_commit.id) + - name: git-repo-name + value: $(body.repository.name) + - name: git-owner + value: $(body.repository.owner.login) + +--- +apiVersion: triggers.tekton.dev/v1beta1 +kind: TriggerTemplate +metadata: + name: ${{ values.repoName }}-git-template +spec: + resourcetemplates: + - apiVersion: tekton.dev/v1 + kind: PipelineRun + metadata: + generateName: ${{ values.repoName }}-run- + labels: + tekton.dev/pipeline: from-code-to-cluster + app.kubernetes.io/instance: ${{ values.repoName }} + spec: + serviceAccountName: pipeline + pipelineRef: + name: from-code-to-cluster + params: + - name: app-repo-url + value: $(tt.params.git-repo-url) + - name: app-repo-revision + value: $(tt.params.git-revision) + - name: image-repo + value: "${{ values.image }}" + - name: GITOPS_REPO_URL + value: "${{ values.gitopsRepo }}" + - name: GITOPS_REPO_BRANCH + value: "main" + - name: MANIFEST_PATH + value: "helios-app.yaml" + - name: docker-secret + value: "docker-credentials" + - name: test-command + value: "${{ values.testCommand }}" + - name: argocd-namespace + value: "argocd" + - name: argocd-app-name + value: "${{ values.repoName }}-argocd" + workspaces: + - name: source-workspace + volumeClaimTemplate: + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + +--- +apiVersion: triggers.tekton.dev/v1beta1 +kind: EventListener +metadata: + name: ${{ values.repoName }}-listener +spec: + serviceAccountName: ${{ values.repoName }}-sa + triggers: + - binding: + name: ${{ values.repoName }}-git-binding + template: + name: ${{ values.repoName }}-git-template + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ${{ values.repoName }}-sa +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: ${{ values.repoName }}-listener-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tekton-triggers-eventlistener-roles +subjects: + - kind: ServiceAccount + name: ${{ values.repoName }}-sa +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ${{ values.repoName }}-listener-clusterbinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tekton-triggers-eventlistener-clusterroles +subjects: + - kind: ServiceAccount + name: ${{ values.repoName }}-sa + namespace: default diff --git a/apps/portal/examples/multi-option-react-template/content/source/.eslintignore b/apps/portal/examples/multi-option-react-template/content/source/.eslintignore new file mode 100644 index 0000000..176c58d --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/.eslintignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +*.config.js +*.config.cjs +postcss.config.cjs +tailwind.config.cjs +vite.config.js +webpack.config.js diff --git a/apps/portal/examples/multi-option-react-template/content/source/.eslintrc.json b/apps/portal/examples/multi-option-react-template/content/source/.eslintrc.json new file mode 100644 index 0000000..d07b507 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/.eslintrc.json @@ -0,0 +1,31 @@ +{ + "env": { + "browser": true, + "es2020": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended" + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "settings": { + "react": { + "version": "18.2" + } + }, + "plugins": [ + "react-refresh" + ], + "rules": { + "react-refresh/only-export-components": [ + "warn", + { "allowConstantExport": true } + ], + "react/prop-types": "off" + } +} diff --git a/apps/portal/examples/multi-option-react-template/content/source/.gitignore b/apps/portal/examples/multi-option-react-template/content/source/.gitignore new file mode 100644 index 0000000..3b6e931 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +.env +.env.local +.eslintcache diff --git a/apps/portal/examples/multi-option-react-template/content/source/Dockerfile b/apps/portal/examples/multi-option-react-template/content/source/Dockerfile new file mode 100644 index 0000000..d3a2f63 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/Dockerfile @@ -0,0 +1,15 @@ +# Build stage +FROM node:22-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# Production stage +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +ARG PORT=${{ values.port }} +RUN sed -i "s/listen\( *\)80;/listen ${PORT};/" /etc/nginx/conf.d/default.conf +EXPOSE ${PORT} +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/portal/examples/multi-option-react-template/content/source/babel.config.json b/apps/portal/examples/multi-option-react-template/content/source/babel.config.json new file mode 100644 index 0000000..08d007e --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/babel.config.json @@ -0,0 +1,6 @@ +{ + "presets": [ + "@babel/preset-env", + ["@babel/preset-react", { "runtime": "automatic" }] + ] +} diff --git a/apps/portal/examples/multi-option-react-template/content/source/catalog-info.yaml b/apps/portal/examples/multi-option-react-template/content/source/catalog-info.yaml new file mode 100644 index 0000000..39928b3 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/catalog-info.yaml @@ -0,0 +1,18 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: ${{ values.repoName | dump }} + description: ${{ (values.description or "") | dump }} + annotations: + gitea.org/repo-url: http://localhost:3030/${{ values.owner }}/${{ values.repoName }} + backstage.io/techdocs-ref: dir:. + backstage.io/kubernetes-id: ${{ values.repoName }} + backstage.io/kubernetes-label-selector: app.kubernetes.io/name=${{ values.repoName }} + backstage.io/kubernetes-namespace: default + janus-idp.io/tekton: ${{ values.repoName }} + tekton.dev/ci-cd: "true" + argocd/app-name: ${{ values.repoName }}-argocd +spec: + type: website + lifecycle: experimental + owner: ${{ values.owner | dump }} diff --git a/apps/portal/examples/multi-option-react-template/content/source/index.vite.html b/apps/portal/examples/multi-option-react-template/content/source/index.vite.html new file mode 100644 index 0000000..0a1335f --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/index.vite.html @@ -0,0 +1,13 @@ + + + + + + + ${{ values.name }} + + +
+ + + diff --git a/apps/portal/examples/multi-option-react-template/content/source/index.webpack.html b/apps/portal/examples/multi-option-react-template/content/source/index.webpack.html new file mode 100644 index 0000000..c3238c8 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/index.webpack.html @@ -0,0 +1,12 @@ + + + + + + + ${{ values.name }} + + +
+ + diff --git a/apps/portal/examples/multi-option-react-template/content/source/package.json b/apps/portal/examples/multi-option-react-template/content/source/package.json new file mode 100644 index 0000000..964a8e8 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/package.json @@ -0,0 +1,62 @@ +{ + "name": "${{ values.repoName | lower }}", + "private": true, + "version": "1.0.0", + {% if values.buildTool == 'vite' -%} + "type": "module", + {%- endif %} + "scripts": { + {% if values.buildTool == 'vite' -%} + "dev": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + {%- else -%} + "dev": "webpack serve --mode development", + "build": "webpack --mode production", + "lint": "eslint src/ --ext js,jsx --report-unused-disable-directives --max-warnings 0" + {%- endif %} + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + {%- if values.stateManagement == 'redux' -%}, + "@reduxjs/toolkit": "^2.2.5", + "react-redux": "^9.1.2" + {%- elif values.stateManagement == 'zustand' -%}, + "zustand": "^4.5.2" + {%- endif %} + {%- if values.dataFetching == 'react-query' -%}, + "@tanstack/react-query": "^5.40.1" + {%- endif %} + }, + "devDependencies": { + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6" + {%- if values.buildTool == 'vite' -%}, + "vite": "^5.2.11", + "@vitejs/plugin-react": "^4.2.1" + {%- else -%}, + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.0.4", + "html-webpack-plugin": "^5.6.0", + "babel-loader": "^9.1.3", + "@babel/core": "^7.24.5", + "@babel/preset-env": "^7.24.5", + "@babel/preset-react": "^7.24.1", + "style-loader": "^4.0.0", + "css-loader": "^7.1.1" + {%- endif %} + {%- if values.styling == 'tailwind' -%}, + "tailwindcss": "^3.4.3", + "postcss": "^8.4.38", + "autoprefixer": "^10.4.19" + {%- if values.buildTool == 'webpack' -%}, + "postcss-loader": "^8.1.1" + {%- endif %} + {%- endif %} + } +} diff --git a/apps/portal/examples/multi-option-react-template/content/source/postcss.config.cjs b/apps/portal/examples/multi-option-react-template/content/source/postcss.config.cjs new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/apps/portal/examples/multi-option-react-template/content/source/src/App.jsx b/apps/portal/examples/multi-option-react-template/content/source/src/App.jsx new file mode 100644 index 0000000..640c7f4 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/src/App.jsx @@ -0,0 +1,117 @@ +{% if values.stateManagement == 'none' or values.dataFetching == 'none' -%} +import * as React from 'react' +{%- endif %} +{% if values.stateManagement == 'zustand' -%} +import { useStore } from './store/store.js' +{%- elif values.stateManagement == 'redux' -%} +import { useSelector, useDispatch } from 'react-redux' +import { increment, decrement } from './store/store.js' +{%- endif %} + +{% if values.dataFetching == 'react-query' -%} +import { useQuery } from '@tanstack/react-query' +{%- endif %} + +function App() { + {% if values.stateManagement == 'zustand' -%} + const { count, increment, decrement } = useStore() + {%- elif values.stateManagement == 'redux' -%} + const count = useSelector((state) => state.counter.value) + const dispatch = useDispatch() + {%- else -%} + const [count, setCount] = React.useState(0) + {%- endif %} + + {% if values.dataFetching == 'react-query' -%} + const { data, isLoading, error } = useQuery({ + queryKey: ['repoData'], + queryFn: async () => { + const res = await fetch('https://api.github.com/repos/facebook/react') + if (!res.ok) { + throw new Error('Network response was not ok') + } + return res.json() + }, + }) + {%- else -%} + const [data, setData] = React.useState(null) + const [isLoading, setIsLoading] = React.useState(false) + const [error, setError] = React.useState(null) + + React.useEffect(() => { + setIsLoading(true) + fetch('https://api.github.com/repos/facebook/react') + .then((res) => { + if (!res.ok) { + throw new Error('Network response was not ok') + } + return res.json() + }) + .then((data) => { + setData(data) + setIsLoading(false) + }) + .catch((err) => { + setError(err) + setIsLoading(false) + }) + }, []) + {%- endif %} + + return ( +
+
+

+ React App Scaffolder Template +

+ +
+

Tech Stack

+
    +
  • Build Tool: ${{ values.buildTool | capitalize }}
  • +
  • Styling: ${{ values.styling | capitalize }}
  • +
  • State Management: ${{ values.stateManagement | capitalize }}
  • +
  • Data Fetching: {% if values.dataFetching == 'react-query' %}React Query{% else %}Standard Fetch{% endif %}
  • +
+
+ + {/* Counter Section */} +
+

Counter State Example (${{ values.stateManagement | capitalize }})

+
+ + {count} + +
+
+ + {/* Data Fetching Section */} +
+

Data Fetching Example ({% if values.dataFetching == 'react-query' %}React Query{% else %}Fetch{% endif %})

+ {isLoading &&

Loading React repo stats...

} + {error &&

Error fetching data: {error.message || 'Unknown error'}

} + {data && ( +
+

Repo Name: {data.full_name}

+

Stars: {data.stargazers_count?.toLocaleString()}

+

Forks: {data.forks_count?.toLocaleString()}

+

Open Issues: {data.open_issues_count?.toLocaleString()}

+
+ )} +
+
+
+ ) +} + +export default App diff --git a/apps/portal/examples/multi-option-react-template/content/source/src/index.tailwind.css b/apps/portal/examples/multi-option-react-template/content/source/src/index.tailwind.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/src/index.tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/portal/examples/multi-option-react-template/content/source/src/index.vanilla.css b/apps/portal/examples/multi-option-react-template/content/source/src/index.vanilla.css new file mode 100644 index 0000000..45dcc8f --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/src/index.vanilla.css @@ -0,0 +1,136 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #0f172a; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +.app-container { + min-height: 100vh; + width: 100vw; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; + box-sizing: border-box; +} + +.app-card { + max-width: 600px; + width: 100%; + background-color: #1e293b; + border-radius: 0.75rem; + padding: 2rem; + box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); + border: 1px solid #334155; + box-sizing: border-box; +} + +.app-title { + font-size: 1.875rem; + line-height: 2.25rem; + font-weight: 700; + margin-top: 0; + margin-bottom: 1.5rem; + background: linear-gradient(to right, #22d3ee, #3b82f6); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.tech-stack-panel { + margin-bottom: 2rem; + padding: 1rem; + background-color: rgba(51, 65, 85, 0.5); + border-radius: 0.5rem; +} + +.section-title { + font-size: 1.125rem; + font-weight: 600; + margin-top: 0; + margin-bottom: 0.5rem; + color: #67e8f9; +} + +.tech-stack-list { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; + font-size: 0.875rem; + list-style-type: none; + padding-left: 0; + margin: 0; +} + +.counter-section { + margin-bottom: 2rem; +} + +.counter-controls { + display: flex; + align-items: center; + gap: 1rem; +} + +.btn { + padding: 0.5rem 1rem; + background-color: #334155; + border: none; + border-radius: 0.25rem; + color: white; + font-weight: 700; + cursor: pointer; + transition: background-color 0.2s; +} + +.btn:hover { + background-color: #475569; +} + +.btn-primary { + background-color: #0891b2; +} + +.btn-primary:hover { + background-color: #06b6d4; +} + +.counter-value { + font-family: monospace; + font-size: 1.5rem; + width: 3rem; + text-align: center; +} + +.data-section { + border-top: 1px solid #334155; + padding-top: 1.5rem; +} + +.stats-grid { + font-size: 0.875rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.error-msg { + color: #f87171; +} diff --git a/apps/portal/examples/multi-option-react-template/content/source/src/main.jsx b/apps/portal/examples/multi-option-react-template/content/source/src/main.jsx new file mode 100644 index 0000000..fd1d130 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/src/main.jsx @@ -0,0 +1,32 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +{% if values.dataFetching == 'react-query' -%} +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +const queryClient = new QueryClient() +{%- endif %} + +{% if values.stateManagement == 'redux' -%} +import { Provider } from 'react-redux' +import { store } from './store/store.js' +{%- endif %} + +ReactDOM.createRoot(document.getElementById('root')).render( + + {% if values.dataFetching == 'react-query' -%} + + {%- endif %} + {% if values.stateManagement == 'redux' -%} + + {%- endif %} + + {% if values.stateManagement == 'redux' -%} + + {%- endif %} + {% if values.dataFetching == 'react-query' -%} + + {%- endif %} + , +) diff --git a/apps/portal/examples/multi-option-react-template/content/source/src/store/reduxStore.js b/apps/portal/examples/multi-option-react-template/content/source/src/store/reduxStore.js new file mode 100644 index 0000000..4192c2e --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/src/store/reduxStore.js @@ -0,0 +1,24 @@ +import { configureStore, createSlice } from '@reduxjs/toolkit' + +const counterSlice = createSlice({ + name: 'counter', + initialState: { + value: 0, + }, + reducers: { + increment: (state) => { + state.value += 1 + }, + decrement: (state) => { + state.value -= 1 + }, + }, +}) + +export const { increment, decrement } = counterSlice.actions + +export const store = configureStore({ + reducer: { + counter: counterSlice.reducer, + }, +}) diff --git a/apps/portal/examples/multi-option-react-template/content/source/src/store/zustandStore.js b/apps/portal/examples/multi-option-react-template/content/source/src/store/zustandStore.js new file mode 100644 index 0000000..6a5863a --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/src/store/zustandStore.js @@ -0,0 +1,7 @@ +import { create } from 'zustand' + +export const useStore = create((set) => ({ + count: 0, + increment: () => set((state) => ({ count: state.count + 1 })), + decrement: () => set((state) => ({ count: state.count - 1 })), +})) diff --git a/apps/portal/examples/multi-option-react-template/content/source/tailwind.config.cjs b/apps/portal/examples/multi-option-react-template/content/source/tailwind.config.cjs new file mode 100644 index 0000000..1c3b7e1 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/tailwind.config.cjs @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/apps/portal/examples/multi-option-react-template/content/source/vite.config.js b/apps/portal/examples/multi-option-react-template/content/source/vite.config.js new file mode 100644 index 0000000..c72ca71 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + }, +}) diff --git a/apps/portal/examples/multi-option-react-template/content/source/webpack.config.ejs b/apps/portal/examples/multi-option-react-template/content/source/webpack.config.ejs new file mode 100644 index 0000000..eb09703 --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/content/source/webpack.config.ejs @@ -0,0 +1,46 @@ +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = { + entry: './src/main.jsx', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'bundle.js', + clean: true, + }, + resolve: { + extensions: ['.js', '.jsx'], + }, + module: { + rules: [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env', '@babel/preset-react'], + }, + }, + }, + { + test: /\.css$/, + use: {% if values.styling == 'tailwind' %}['style-loader', 'css-loader', 'postcss-loader']{% else %}['style-loader', 'css-loader']{% endif %}, + }, + ], + }, + plugins: [ + new HtmlWebpackPlugin({ + template: './index.html', + }), + ], + devServer: { + static: { + directory: path.join(__dirname, 'public'), + }, + compress: true, + port: 3000, + hot: true, + historyApiFallback: true, + }, +}; diff --git a/apps/portal/examples/multi-option-react-template/template.yaml b/apps/portal/examples/multi-option-react-template/template.yaml new file mode 100644 index 0000000..bb034cc --- /dev/null +++ b/apps/portal/examples/multi-option-react-template/template.yaml @@ -0,0 +1,236 @@ +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: multi-option-react-template + title: Multi-Option React Scaffolder Template + description: A flexible React template that lets you customize your build tool (Vite/Webpack), styling (Tailwind CSS), state management (Zustand/Redux), and data fetching (React Query), and deploys it automatically. +spec: + owner: user:guest + type: service + + parameters: + - title: Component Details + required: + - name + - port + - dockerOrg + - repoName + properties: + name: + title: Name + type: string + description: Unique name of the component + ui:autofocus: true + port: + title: Port + type: number + description: The port to expose + default: 8080 + dockerOrg: + title: Docker Registry Org/User + type: string + description: Your Docker Hub username or Organization + default: phamhoangkha1403 + repoName: + title: Docker Repository Name + type: string + description: The name of the repository (e.g. my-react-app) + pattern: "^(?:@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9-~][a-z0-9-._~]*$" + description: + title: Description + type: string + description: A brief description of the component + - title: Tech Stack Options + required: + - buildTool + - styling + - stateManagement + - dataFetching + properties: + buildTool: + title: Build Tool + type: string + description: Select the build tool and bundler + enum: + - vite + - webpack + enumNames: + - Vite + - Webpack + default: vite + styling: + title: Styling + type: string + description: Choose the styling method + enum: + - tailwind + - vanilla + enumNames: + - Tailwind CSS + - Vanilla CSS + default: tailwind + stateManagement: + title: State Management + type: string + description: Choose a state management library + enum: + - none + - zustand + - redux + enumNames: + - None + - Zustand + - Redux (Toolkit) + default: none + dataFetching: + title: Data Fetching + type: string + description: Choose a data fetching library + enum: + - none + - react-query + enumNames: + - None (Standard fetch) + - React Query (TanStack Query) + default: none + - title: Choose a location + required: + - repoUrl + - webhookSecret + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - localhost:3030 + webhookSecret: + title: Webhook Secret + type: string + description: A secure random secret to secure the Git webhook + ui:field: Secret + + steps: + # 1. Fetch Source Code Template + - id: fetch-source + name: Fetch Source Code Template + action: fetch:template + input: + url: ./content/source + targetPath: ./source + values: + name: ${{ parameters.name }} + repoName: ${{ parameters.repoName }} + description: ${{ parameters.description }} + buildTool: ${{ parameters.buildTool }} + styling: ${{ parameters.styling }} + stateManagement: ${{ parameters.stateManagement }} + dataFetching: ${{ parameters.dataFetching }} + port: ${{ parameters.port }} + owner: ${{ (parameters.repoUrl | parseRepoUrl).owner }} + image: index.docker.io/${{ parameters.dockerOrg }}/${{ parameters.repoName }} + + # 2. Configure Selected Stack (Always run, dynamic list) + # File rename logic: + # - index.{buildTool}.html -> index.html (vite or webpack) + # - src/index.{styling}.css -> src/index.css (tailwind or vanilla) + # - src/store/{redux|zustand}Store.js -> src/store/store.js (if not "none") + # - webpack.config.ejs -> webpack.config.js (if webpack) + - id: configure-files + name: Configure Selected Stack + action: fs:rename + input: + files: '${{ [ { from: "./source/index." + parameters.buildTool + ".html", to: "./source/index.html" }, { from: "./source/src/index." + parameters.styling + ".css", to: "./source/src/index.css" } ].concat( [ { from: "./source/src/store/reduxStore.js", to: "./source/src/store/store.js" } ] if parameters.stateManagement == "redux" else ( [ { from: "./source/src/store/zustandStore.js", to: "./source/src/store/store.js" } ] if parameters.stateManagement == "zustand" else [] ) ).concat( [ { from: "./source/webpack.config.ejs", to: "./source/webpack.config.js" } ] if parameters.buildTool == "webpack" else [] ) }}' + + # 3. Clean up Unused Files (Always run, dynamic list) + - id: cleanup-files + name: Clean up Unused Files + action: fs:delete + input: + files: '${{ ( [ "./source/index.webpack.html", "./source/webpack.config.ejs", "./source/babel.config.json" ] if parameters.buildTool == "vite" else [ "./source/index.vite.html", "./source/vite.config.js" ] ).concat( [ "./source/tailwind.config.cjs", "./source/postcss.config.cjs" ] if parameters.styling == "vanilla" else [] ).concat( [ "./source/src/store/zustandStore.js" ] if parameters.stateManagement == "redux" else ( [ "./source/src/store/reduxStore.js" ] if parameters.stateManagement == "zustand" else [ "./source/src/store" ] ) ).concat( [ "./source/src/index.vanilla.css" ] if parameters.styling == "tailwind" else [ "./source/src/index.tailwind.css" ] ) }}' + + # 4. Publish Source Code to Gitea + - id: publish-source + name: Publish Source Code + action: publish:gitea + input: + description: Source Code for ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} + sourcePath: ./source + repoVisibility: public + + # 5. Create Gitea Webhook for Tekton CI + - id: create-webhook + name: Create Webhook + action: gitea:create-webhook + input: + repoUrl: ${{ parameters.repoUrl }} + webhookUrl: http://el-${{ parameters.repoName }}-listener.default.svc.cluster.local:8080 + webhookSecret: ${{ secrets.webhookSecret }} + events: + - push + + # 6. Fetch GitOps Manifests Template + - id: fetch-gitops + name: Fetch GitOps Manifests + action: fetch:template + input: + url: ./content/gitops + targetPath: ./gitops + values: + name: ${{ parameters.name }} + repoName: ${{ parameters.repoName }} + image: index.docker.io/${{ parameters.dockerOrg }}/${{ parameters.repoName }} + dockerOrg: ${{ parameters.dockerOrg }} + port: ${{ parameters.port }} + owner: ${{ (parameters.repoUrl | parseRepoUrl).owner }} + sourceRepo: ${{ steps['publish-source'].output.remoteUrl }} + gitopsRepo: ${{ steps['publish-source'].output.remoteUrl | replace(".git", "") }}-gitops + testCommand: "npm install && npm run build" + + # 7. Publish GitOps Manifests to Gitea + - id: publish-gitops + name: Publish GitOps Manifests + action: publish:gitea + input: + description: GitOps Manifests for ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }}-gitops + sourcePath: ./gitops + repoVisibility: public + + # 8. Create Git Credentials Secret on Kubernetes + - id: create-secret + name: Create Git Credentials Secret + action: kubernetes:create-git-credentials-secret + input: + name: ${{ parameters.repoName }} + namespace: default + username: ${{ (parameters.repoUrl | parseRepoUrl).owner }} + webhookSecret: ${{ secrets.webhookSecret }} + + # 9. Deploy HeliosApp to Kubernetes + - id: apply-helios + name: Deploy to Kubernetes + action: kubernetes:apply + input: + manifestPath: ./gitops/helios-app.yaml + namespaced: true + + # 10. Register Component in Catalog + - id: register + name: Register Component + action: catalog:register + input: + repoContentsUrl: ${{ steps['publish-source'].output.repoContentsUrl }} + catalogInfoPath: 'catalog-info.yaml' + + output: + links: + - title: Source Repository + url: ${{ steps['publish-source'].output.remoteUrl }} + - title: GitOps Repository + url: ${{ steps['publish-gitops'].output.remoteUrl }} + - title: Open in Catalog + icon: catalog + entityRef: ${{ steps['register'].output.entityRef }}