@ossy/platform 0.0.1-alpha.2 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,40 +1,37 @@
1
1
  {
2
2
  "name": "@ossy/platform",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "1.14.0",
4
+ "description": "Ossy application server runtime",
4
5
  "repository": {
5
6
  "type": "git",
6
- "url": "git+https://github.com/ossy-se/ossy.git",
7
- "directory": "packages/platform"
7
+ "url": "git+https://github.com/ossy-se/packages.git"
8
8
  },
9
9
  "type": "module",
10
- "description": "Platform module: deployment platform config resource template and home UI for Ossy websites",
10
+ "main": "./src/server.js",
11
11
  "exports": {
12
- ".": "./src/index.js",
13
- "./package.json": "./package.json"
12
+ ".": "./src/server.js",
13
+ "./server": "./src/server.js",
14
+ "./proxy-internal": "./src/proxy-internal.js",
15
+ "./worker": "./src/worker-entry.js",
16
+ "./worker-runtime": "./src/worker-runtime.js"
14
17
  },
15
- "main": "./src/index.js",
16
- "sideEffects": false,
17
18
  "scripts": {
18
- "build": "echo \"@ossy/platform: no build step\" && exit 0",
19
- "test": ""
19
+ "start": "PORT=3003 node ./src/server.js"
20
20
  },
21
+ "keywords": [],
21
22
  "author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
22
23
  "license": "MIT",
24
+ "dependencies": {
25
+ "@ossy/router": "^1.15.0",
26
+ "@ossy/sdk": "^1.15.0",
27
+ "cookie-parser": "^1.4.7",
28
+ "express": ">=5.0.0 <6.0.0",
29
+ "morgan": ">=1.10.1 <2.0.0",
30
+ "react": ">=19.0.0 <20.0.0",
31
+ "react-dom": ">=19.0.0 <20.0.0"
32
+ },
23
33
  "files": [
24
- "src",
25
- "README.md",
26
- "CHANGELOG.md"
34
+ "src"
27
35
  ],
28
- "publishConfig": {
29
- "access": "public",
30
- "registry": "https://registry.npmjs.org"
31
- },
32
- "peerDependencies": {
33
- "@ossy/connected-components": ">=0.14.0",
34
- "@ossy/design-system": ">=0.14.0",
35
- "@ossy/router-react": ">=0.14.0",
36
- "@ossy/sdk-react": ">=0.14.0",
37
- "react": ">=19.0.0"
38
- },
39
- "gitHead": "280ce1f62d7daf721909ac5eabe4181991e00456"
36
+ "gitHead": "6a7aac8937521487dc9a032a8fe71f02f2a86367"
40
37
  }
package/src/http.js ADDED
@@ -0,0 +1,85 @@
1
+ import path from 'path';
2
+ import url from 'url'
3
+ import express from 'express'
4
+ import morgan from 'morgan'
5
+ import { Router as OssyRouter } from '@ossy/router'
6
+ import cookieParser from 'cookie-parser'
7
+
8
+ const app = express();
9
+
10
+ const currentDir = path.dirname(url.fileURLToPath(import.meta.url))
11
+
12
+ function parsePortFromArgv(argv) {
13
+ // Supports: --port 4000, --port=4000, -p 4000
14
+ const idx = argv.findIndex(a => a === '--port' || a === '-p')
15
+ if (idx !== -1 && argv[idx + 1]) return argv[idx + 1]
16
+
17
+ const eq = argv.find(a => a.startsWith('--port='))
18
+ if (eq) return eq.split('=')[1]
19
+
20
+ return undefined
21
+ }
22
+
23
+ function normalizePort(value, fallback) {
24
+ if (value === undefined || value === null || value === '') return fallback
25
+ const n = Number.parseInt(String(value), 10)
26
+ if (!Number.isFinite(n) || n <= 0) return fallback
27
+ return n
28
+ }
29
+
30
+ const DEFAULT_PORT = 3000
31
+ const port = normalizePort(parsePortFromArgv(process.argv) ?? process.env.PORT, DEFAULT_PORT)
32
+
33
+ const middleware = [
34
+ morgan('tiny'),
35
+ express.json({ strict: false }),
36
+ cookieParser(process.env.OSSY_COOKIE_SECRET || 'default_secret'),
37
+ ]
38
+
39
+ app.use(middleware)
40
+
41
+ // const PageRegistry = OssyRouter.of({ pages: [] })
42
+ // const ApiRegistry = OssyRouter.of({ api: [] })
43
+ // const TaskRegistry = OssyRouter.of({ tasks: [] })
44
+ // const StaticRegistry = OssyRouter.of({ static: [] })
45
+
46
+ const Registry = OssyRouter.of({ pages: [
47
+ {
48
+ id: 'home',
49
+ type: '@ossy/platform/page',
50
+ path: '/',
51
+ content: '<h1>Hello World from page route</h1>'
52
+ },
53
+ {
54
+ id: 'home',
55
+ type: '@ossy/platform/api',
56
+ path: '/api',
57
+ handle: (req, res) => {
58
+ res.send({ message: 'Hello World from api route' })
59
+ }
60
+ }
61
+ ]})
62
+
63
+ app.all('*all', (req, res) => {
64
+ const pathname = req.originalUrl
65
+
66
+ // Will be replaced with fetching the resource from ossy storage
67
+ const resource = Registry.getPageByUrl(pathname)
68
+
69
+ if (resource.type === '@ossy/platform/page') {
70
+ res.send(resource.content)
71
+ }
72
+
73
+ else if (resource.type === '@ossy/platform/api') {
74
+ resource.handle(req, res)
75
+ }
76
+
77
+ else {
78
+ res.status(404).send('Not found')
79
+ }
80
+
81
+ })
82
+
83
+ app.listen(port, () => {
84
+ console.log(`[@ossy/app][server] Running on http://localhost:${port}`);
85
+ });
@@ -0,0 +1,112 @@
1
+ /** Node/Express can join duplicate workspace headers as `id, id` — API expects a single id. */
2
+ function normalizeWorkspaceIdHeader (value) {
3
+ if (!value || value === 'undefined') return undefined
4
+ const first = String(value).split(',')[0]?.trim()
5
+ return first || undefined
6
+ }
7
+
8
+ export function ProxyInternal () {
9
+ return (req, res, next) => {
10
+ if (!req.originalUrl.startsWith('/@ossy')) {
11
+ return next()
12
+ }
13
+
14
+ if (req.originalUrl.startsWith('/@ossy/users/me/app-settings') && req.method === 'PATCH') {
15
+ if (!req.body) {
16
+ res.status(400)
17
+ res.json('Invalid request body')
18
+ return
19
+ }
20
+
21
+ const requestedSettings = req.body
22
+ const expiresMaxAge = 2147483647
23
+ const userSettings = JSON.parse(req.signedCookies?.['x-ossy-user-settings'] || '{}')
24
+
25
+ const updatedSettings = {
26
+ ...userSettings,
27
+ ...requestedSettings,
28
+ }
29
+
30
+ res.cookie('x-ossy-user-settings', JSON.stringify(updatedSettings), {
31
+ httpOnly: true,
32
+ signed: true,
33
+ expires: new Date(Date.now() + expiresMaxAge),
34
+ })
35
+
36
+ res.status(201)
37
+ res.json('')
38
+ return
39
+ }
40
+
41
+ if (req.originalUrl.startsWith('/@ossy/users/me/app-settings') && req.method === 'GET') {
42
+ console.log('[@ossy/platform][proxy] GET /@ossy/users/me/app-settings')
43
+ const userSettings = JSON.parse(req.signedCookies?.['x-ossy-user-settings'] || '{}')
44
+ res.status(200)
45
+ res.json(userSettings)
46
+ return
47
+ }
48
+
49
+ console.log(`[@ossy/platform][proxy] ${req.method} ${req.originalUrl}`)
50
+
51
+ const domain = process.env.OSSY_API_URL || 'https://api.ossy.se'
52
+ const url = `${domain}${req.originalUrl?.replace('/@ossy', '/api/v0')}`
53
+ const forwardedHeaders = JSON.parse(JSON.stringify(req.headers))
54
+ const workspaceId = normalizeWorkspaceIdHeader(req.get('workspaceId'))
55
+
56
+ if (workspaceId) {
57
+ forwardedHeaders.workspaceid = workspaceId
58
+ }
59
+
60
+ const request = {
61
+ method: req.method,
62
+ headers: forwardedHeaders,
63
+ }
64
+
65
+ if (!['GET', 'HEAD'].includes(req.method)) {
66
+ request.body = JSON.stringify(req.body)
67
+ }
68
+
69
+ fetch(url, request)
70
+ .then((response) => {
71
+ response.headers.forEach((value, name) => {
72
+ res.setHeader(name, value)
73
+ })
74
+
75
+ if (response.headers.get('content-type')?.includes('application/json')) {
76
+ return response.text().then((text) => {
77
+ const trimmed = text?.trim() ?? ''
78
+ let data
79
+ try {
80
+ data = trimmed === '' ? null : JSON.parse(trimmed)
81
+ } catch (error) {
82
+ console.log('[@ossy/platform][proxy][error]', error)
83
+ res.removeHeader('content-length')
84
+ const st = response.status
85
+ if (st === 401 || st === 403 || st === 404) {
86
+ res.status(st)
87
+ res.json(null)
88
+ return
89
+ }
90
+ res.status(502)
91
+ res.json({ message: 'Upstream returned invalid JSON' })
92
+ return
93
+ }
94
+ res.removeHeader('content-length')
95
+ res.status(response.status)
96
+ res.json(data)
97
+ })
98
+ }
99
+
100
+ return response.arrayBuffer().then((buffer) => {
101
+ res.status(response.status)
102
+ res.send(Buffer.from(buffer))
103
+ })
104
+ })
105
+ .catch((error) => {
106
+ console.log('[@ossy/platform][proxy][error]', error)
107
+ const status = error.status
108
+ res.status(status || 500)
109
+ res.json({ message: error.message || 'Internal Server Error' })
110
+ })
111
+ }
112
+ }
package/src/server.js ADDED
@@ -0,0 +1,142 @@
1
+ import fs from 'node:fs'
2
+ import path from 'path'
3
+ import url from 'url'
4
+ import { pathToFileURL } from 'node:url'
5
+ import express from 'express'
6
+ import morgan from 'morgan'
7
+ import { Router as OssyRouter } from '@ossy/router'
8
+ import cookieParser from 'cookie-parser'
9
+ import { ProxyInternal } from './proxy-internal.js'
10
+
11
+ import buildTimeConfig from './.ossy/server-config.runtime.mjs'
12
+ import { BuildPage, buildPrerenderAppConfig } from './.ossy/render-page.task.js'
13
+ import Middleware from './.ossy/middleware.runtime.js'
14
+
15
+ const __ossyDir = path.dirname(url.fileURLToPath(import.meta.url)) + '/.ossy'
16
+
17
+ function readOssyJson (name) {
18
+ return JSON.parse(fs.readFileSync(path.join(__ossyDir, name), 'utf8'))
19
+ }
20
+
21
+ const apiRouteList = readOssyJson('api.generated.json') ?? []
22
+ const sitePageList = readOssyJson('pages.generated.json') ?? []
23
+
24
+ function pageRouterLanguageOptions (config, pages) {
25
+ let supported = config?.supportedLanguages
26
+ let defaultLanguage = config?.defaultLanguage
27
+ if ((!supported || supported.length <= 1) && pages.length > 0) {
28
+ const p0 = pages[0]
29
+ if (p0 && typeof p0.path === 'object' && p0.path != null) {
30
+ supported = Object.keys(p0.path)
31
+ defaultLanguage = defaultLanguage || supported[0]
32
+ }
33
+ }
34
+ return {
35
+ supportedLanguages: Array.isArray(supported) ? supported : [],
36
+ defaultLanguage,
37
+ }
38
+ }
39
+
40
+ const app = express()
41
+
42
+ const currentDir = path.dirname(url.fileURLToPath(import.meta.url))
43
+ const ROOT_PATH = path.resolve(currentDir, 'public')
44
+
45
+ function parsePortFromArgv (argv) {
46
+ const idx = argv.findIndex((a) => a === '--port' || a === '-p')
47
+ if (idx !== -1 && argv[idx + 1]) return argv[idx + 1]
48
+
49
+ const eq = argv.find((a) => a.startsWith('--port='))
50
+ if (eq) return eq.split('=')[1]
51
+
52
+ return undefined
53
+ }
54
+
55
+ function normalizePort (value, fallback) {
56
+ if (value === undefined || value === null || value === '') return fallback
57
+ const n = Number.parseInt(String(value), 10)
58
+ if (!Number.isFinite(n) || n <= 0) return fallback
59
+ return n
60
+ }
61
+
62
+ const DEFAULT_PORT = 3000
63
+ const port = normalizePort(parsePortFromArgv(process.argv) ?? process.env.PORT, DEFAULT_PORT)
64
+
65
+ if (Middleware !== undefined) {
66
+ console.log(`[@ossy/platform][server] ${Middleware?.length || 0} custom middleware loaded`)
67
+ }
68
+
69
+ const middleware = [
70
+ morgan('tiny'),
71
+ express.json({ strict: false }),
72
+ cookieParser(process.env.OSSY_COOKIE_SECRET || 'default_secret'),
73
+ (req, _res, next) => {
74
+ const userSettings = JSON.parse(req.signedCookies?.['x-ossy-user-settings'] || '{}')
75
+ req.userAppSettings = userSettings
76
+ if (userSettings.workspaceId && !req.get('workspaceId')) {
77
+ req.headers.workspaceid = userSettings.workspaceId
78
+ }
79
+ const cookieHeader = req.headers.cookie
80
+ req.isAuthenticated = cookieHeader ? cookieHeader.includes('auth=') : false
81
+ next()
82
+ },
83
+ ...(Middleware || []),
84
+ express.static(ROOT_PATH),
85
+ ProxyInternal(),
86
+ ]
87
+
88
+ app.use(middleware)
89
+
90
+ const apiRouter = OssyRouter.of({
91
+ pages: apiRouteList,
92
+ })
93
+
94
+ const { supportedLanguages, defaultLanguage } = pageRouterLanguageOptions(
95
+ buildTimeConfig,
96
+ sitePageList
97
+ )
98
+
99
+ const pageRouter = OssyRouter.of({
100
+ pages: sitePageList,
101
+ defaultLanguage,
102
+ supportedLanguages,
103
+ })
104
+
105
+ app.all('*all', async (req, res) => {
106
+ const requestUrl = req.originalUrl || '/'
107
+ try {
108
+ const apiRoute = apiRouter.getPageByUrl(requestUrl)
109
+ if (apiRoute?.module) {
110
+ const mod = await import(pathToFileURL(path.resolve(__ossyDir, apiRoute.module)).href)
111
+ await mod.default(req, res)
112
+ return
113
+ }
114
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
115
+ res.status(404).send('Not found')
116
+ return
117
+ }
118
+ const pageRoute = pageRouter.getPageByUrl(requestUrl)
119
+ if (pageRoute) {
120
+ const appConfig = buildPrerenderAppConfig({
121
+ buildTimeConfig,
122
+ pageList: sitePageList,
123
+ activeRouteId: pageRoute.id,
124
+ urlPath: requestUrl,
125
+ isAuthenticated: !!req.isAuthenticated,
126
+ })
127
+ const html = await BuildPage.handle({ route: pageRoute, appConfig })
128
+ res.status(200).type('html').send(html)
129
+ return
130
+ }
131
+ res.status(404).send('Not found')
132
+ } catch (err) {
133
+ console.error('[@ossy/platform][server] Request handling failed:', err)
134
+ if (!res.headersSent) {
135
+ res.status(500).type('text').send('Internal Server Error')
136
+ }
137
+ }
138
+ })
139
+
140
+ app.listen(port, () => {
141
+ console.log(`[@ossy/platform][server] Running on http://localhost:${port}`)
142
+ })
@@ -0,0 +1,10 @@
1
+ import 'dotenv/config'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { runWorkerScheduler } from './worker-runtime.js'
6
+
7
+ const __ossyDir = path.dirname(fileURLToPath(import.meta.url)) + '/.ossy'
8
+ const tasks = JSON.parse(fs.readFileSync(path.join(__ossyDir, 'tasks.generated.json'), 'utf8')) ?? []
9
+
10
+ runWorkerScheduler(tasks, __ossyDir)
@@ -0,0 +1,115 @@
1
+ import path from 'node:path'
2
+ import { pathToFileURL } from 'node:url'
3
+ import { SDK } from '@ossy/sdk'
4
+
5
+ /**
6
+ * @param {Array<{ type: string, module: string }>} tasks
7
+ * @param {string} ossyDir - absolute path to the .ossy directory
8
+ */
9
+ export function runWorkerScheduler (tasks, ossyDir) {
10
+ const sdk = SDK.of({
11
+ workspaceId: process.env.OSSY_WORKSPACE_ID,
12
+ apiUrl: process.env.OSSY_API_URL,
13
+ authorization: process.env.OSSY_API_TOKEN,
14
+ })
15
+
16
+ const jobsClient = /** @type {{ getUnprocessed: () => Promise<unknown[]> }} */ (sdk.jobs)
17
+ let status = 'running'
18
+
19
+ console.log('Starting scheduler')
20
+ main()
21
+
22
+ setInterval(() => {
23
+ if (status === 'running') return
24
+ status = 'running'
25
+ try {
26
+ main()
27
+ } catch (error) {
28
+ console.log('Error running main')
29
+ console.error(error)
30
+ status = 'idle'
31
+ }
32
+ }, 3000)
33
+
34
+ function main () {
35
+ console.log('Looking for jobs')
36
+ jobsClient
37
+ .getUnprocessed()
38
+ .then(async (jobs) => {
39
+ if (!jobs || !jobs.length) {
40
+ console.log('No jobs found, going idle')
41
+ status = 'idle'
42
+ return
43
+ }
44
+
45
+ const jobsGroupedByResourceId = groupJobsByResourceId(jobs)
46
+ console.log(`Found ${jobs.length} jobs between ${jobsGroupedByResourceId.length} resources`)
47
+
48
+ const processedGroups = jobsGroupedByResourceId.map(([resourceId, groupJobs]) => {
49
+ console.log(`Processing group for resourceId ${resourceId}`)
50
+ return processJobsSequentially(groupJobs)
51
+ .then(() => console.log(`Completed group for resourceId ${resourceId}`))
52
+ .catch((err) => {
53
+ console.log(`Failed to process group for resourceId ${resourceId}`)
54
+ console.error(err)
55
+ })
56
+ })
57
+
58
+ try {
59
+ await Promise.allSettled(processedGroups)
60
+ console.log('Finished processing of groups...')
61
+ console.log('Going idle')
62
+ status = 'idle'
63
+ console.log('----------------------------------')
64
+ console.groupEnd()
65
+ } catch (error) {
66
+ console.log('Error processing groups')
67
+ console.error(error)
68
+ status = 'idle'
69
+ }
70
+ })
71
+ .catch((error) => {
72
+ console.log('Error getting jobs')
73
+ console.error(error)
74
+ status = 'idle'
75
+ })
76
+ }
77
+
78
+ function groupJobsByResourceId (jobList) {
79
+ return Object.entries(
80
+ jobList.reduce((acc, job) => {
81
+ const content = /** @type {{ resourceId?: string }} */ (job.content || {})
82
+ const rid = content.resourceId
83
+ return {
84
+ ...acc,
85
+ [rid]: [...(acc[rid] || []), job],
86
+ }
87
+ }, /** @type {Record<string, unknown[]>} */ ({}))
88
+ )
89
+ }
90
+
91
+ async function processJobsSequentially (jobList) {
92
+ console.log(`Processing ${jobList.length} jobs`)
93
+ for (const job of jobList) {
94
+ console.log(`Processing job ${job.id}`)
95
+ const task = tasks.find((t) => t.type === job.type)
96
+ if (!task) {
97
+ console.log('No handler found for job', job.id)
98
+ continue
99
+ }
100
+
101
+ console.log(`Handler found for ${task.type}`)
102
+ try {
103
+ const mod = await import(pathToFileURL(path.resolve(ossyDir, task.module)).href)
104
+ const jobSdk = SDK.of({
105
+ workspaceId: job.belongsTo,
106
+ authorization: process.env.OSSY_API_TOKEN,
107
+ })
108
+ await mod.default({ sdk: jobSdk, job }).catch(() => {})
109
+ } catch (error) {
110
+ console.error(error)
111
+ console.log('Failed to processing job')
112
+ }
113
+ }
114
+ }
115
+ }
package/CHANGELOG.md DELETED
@@ -1,30 +0,0 @@
1
- # Change Log
2
-
3
- All notable changes to this project will be documented in this file.
4
- See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
-
6
- ## 0.0.1-alpha.2 (2026-03-29)
7
-
8
- **Note:** Version bump only for package @ossy/platform
9
-
10
-
11
-
12
-
13
-
14
- ## 0.0.1-alpha.1 (2026-03-29)
15
-
16
- **Note:** Version bump only for package @ossy/platform
17
-
18
-
19
-
20
-
21
-
22
- # Changelog
23
-
24
- ## Unreleased
25
-
26
- - Platform home route ships from **`src/platform/home.page.js`**; sites add **`@ossy/platform`** to **`modules`** (formerly `pagesModules`) and provide **`src/page-shell.jsx`** (see README).
27
-
28
- ## 0.0.1-alpha.0
29
-
30
- - Initial extract from `website-ossy`: `Definition`, `PlatformResourceTemplates`, `PlatformHomeBody`, `platformHomeMetadata`.
package/README.md DELETED
@@ -1,34 +0,0 @@
1
- # @ossy/platform
2
-
3
- Ossy **Platform** module: resource template `@ossy/platform/config` (mirrors deployment-tools `platform-config.json`) and the platform home UI (`PlatformHomeBody`, metadata for routing).
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install @ossy/platform
9
- ```
10
-
11
- Peer dependencies (your app should already include them):
12
-
13
- - `react`
14
- - `@ossy/connected-components` (for **`usePageShell`** on module pages)
15
- - `@ossy/design-system`
16
- - `@ossy/router-react`
17
- - `@ossy/sdk-react`
18
-
19
- ## Use in a website
20
-
21
- 1. Spread **`PlatformResourceTemplates`** into your app `resourceTemplates` (and sync to the API via publish / cms upload).
22
- 2. Add **`@ossy/platform`** to **`modules`** in `src/config.js` so **`src/platform/home.page.js`** is picked up (JSX-free so Rollup can parse `.page.js`; **`*.page.jsx`** works under `src/` for other files if the bundler accepts JSX there).
23
- 3. Add **`src/page-shell.jsx`** (or `.js`) that default-exports your site layout (e.g. ossy.se re-exports **`Layout`**). Module pages call **`usePageShell()`** so they do not depend on site paths.
24
- 4. Register **`Definition`** in your navigation / module registry.
25
-
26
- **Surface in this package:** **`*.page.*`** files may live anywhere under **`src/`**; resources are exported from the package root (`PlatformResourceTemplates`, constants). **API routes** and **worker tasks** can follow the same `*.api.js` / `*.task.js` conventions under the site `src/` today; pulling those from installable modules can mirror **`modules`** later (e.g. shared discovery for APIs / worker tasks).
27
-
28
- ## Publishing
29
-
30
- Released on its **own semver** from this monorepo (`lerna publish` with independent versions). Bump **`@ossy/platform`** when only this module changes.
31
-
32
- ## Direction
33
-
34
- Long term, **which modules a workspace may enable** should be driven by the **CMS** and tied to **billing / entitlements**. Packages stay the **distributable implementation** of each module; access control lives in the product, not in npm scope alone.
package/src/Definition.js DELETED
@@ -1,13 +0,0 @@
1
- export const Definition = {
2
- id: 'platform',
3
- title: 'Platform',
4
- description: 'Deployment platform configuration aligned with deployment-tools S3 platform-config.json.',
5
- module: {
6
- id: 'platform',
7
- enabled: true,
8
- },
9
- statuses: ['beta'],
10
- actions: ['resources.create', 'resources.update-content', 'resources.rename', 'resources.remove'],
11
- views: ['home.page', 'resources.search', 'resources.get'],
12
- tasks: [],
13
- }
@@ -1,88 +0,0 @@
1
- import React from 'react'
2
- import { useRouter } from '@ossy/router-react'
3
- import { useResources, AsyncStatus } from '@ossy/sdk-react'
4
- import { Title, Text, View, Tags, Button, Switch } from '@ossy/design-system'
5
- import { Definition } from './Definition.js'
6
- import { moduleStatusTags } from './moduleStatus.js'
7
- import { ResourceList } from './ResourceList.jsx'
8
- import { PLATFORM_CONFIG_TEMPLATE_ID, PLATFORM_RESOURCE_LOCATION } from './constants.js'
9
-
10
- /**
11
- * Platform module home content (no site chrome). Wrap with your app {@code Layout} in a *.page.jsx file.
12
- */
13
- export function PlatformHomeBody () {
14
- const router = useRouter()
15
- const { status, resources } = useResources(PLATFORM_RESOURCE_LOCATION)
16
- const platformResources = (resources || []).filter(
17
- (resource) => resource?.type === PLATFORM_CONFIG_TEMPLATE_ID
18
- )
19
-
20
- let listStatus = status
21
-
22
- if (status === AsyncStatus.Success && platformResources.length === 0) {
23
- listStatus = 'Empty'
24
- }
25
-
26
- return (
27
- <View
28
- gap="m"
29
- surface="primary"
30
- style={{ padding: 'var(--space-m) var(--space-l)', height: '100%', overflowY: 'auto' }}
31
- >
32
- <View inset="s" gap="s">
33
- <View layout="row" justifyContent="space-between" alignItems="center">
34
- <View layout="row" gap="s" alignItems="center">
35
- <Title>{Definition.title}</Title>
36
- {moduleStatusTags(Definition).length > 0 && (
37
- <Tags tags={moduleStatusTags(Definition)} size="s" />
38
- )}
39
- </View>
40
- <Button
41
- variant="cta"
42
- prefix="add"
43
- href={router.getHref(
44
- `@create-document?location=${encodeURIComponent(PLATFORM_RESOURCE_LOCATION)}&templateId=${encodeURIComponent(PLATFORM_CONFIG_TEMPLATE_ID)}`
45
- )}
46
- >
47
- New platform config
48
- </Button>
49
- </View>
50
-
51
- <Text style={{ maxWidth: '640px' }}>
52
- {Definition.description} Field names match deployment-tools{' '}
53
- <strong>platform-config.json</strong> on the S3 bucket (excluding <strong>awsRoleToAssume</strong>).
54
- </Text>
55
- </View>
56
-
57
- <View gap="m" inset="s" style={{ overflowY: 'auto' }}>
58
- <Switch on={listStatus}>
59
- <Switch.Case match={[AsyncStatus.NotInitialized, AsyncStatus.Loading]}>
60
- <Text>Loading…</Text>
61
- </Switch.Case>
62
-
63
- <Switch.Case match={[AsyncStatus.Error, AsyncStatus.AuthenticationError]}>
64
- <Text>Could not load platform configs.</Text>
65
- </Switch.Case>
66
-
67
- <Switch.Case match={['Empty']}>
68
- <Text>No platform configs yet. Create one to mirror a deployment-tools platform-config.json.</Text>
69
- </Switch.Case>
70
-
71
- <Switch.Case match={[AsyncStatus.Success]}>
72
- <ResourceList
73
- resources={platformResources.map((resource) => ({
74
- ...resource,
75
- name:
76
- resource?.content?.platformName ||
77
- resource?.content?.platformId ||
78
- resource?.name ||
79
- 'Unnamed platform',
80
- href: router.getHref({ id: '@resource', params: { resourceId: resource?.id } }),
81
- }))}
82
- />
83
- </Switch.Case>
84
- </Switch>
85
- </View>
86
- </View>
87
- )
88
- }
@@ -1,138 +0,0 @@
1
- 'use client'
2
- import React from 'react'
3
- import { useWorkspace } from '@ossy/sdk-react'
4
- import {
5
- Dropdown,
6
- DropZone,
7
- Icon2,
8
- Image,
9
- View,
10
- Text,
11
- Button,
12
- ContextMenu,
13
- } from '@ossy/design-system'
14
- import { formatBytes } from './utils/format-bytes.js'
15
-
16
- export const ResourceList = ({
17
- resources = [],
18
- onClick = () => {},
19
- }) => {
20
- return resources
21
- .map((resource) => (
22
- <ResourceListItem
23
- {...resource}
24
- key={resource.id}
25
- onClick={() => {
26
- onClick(resource)
27
- resource?.onClick?.(resource)
28
- }}
29
- />
30
- ))
31
- }
32
-
33
- function ResourceListItem ({
34
- onClick,
35
- href,
36
- onDrop,
37
- dragData,
38
- ...resource
39
- }) {
40
- let Container = ({ children }) => <>{children}</>
41
-
42
- if (onDrop) {
43
- Container = DropZone
44
- } else if (dragData) {
45
- Container = DropZone.Dragable
46
- }
47
-
48
- return (
49
- <Container onDrop={onDrop} dragData={dragData}>
50
- <View
51
- layout="row"
52
- selectable
53
- gap="m"
54
- style={{
55
- height: '56px',
56
- flexShrink: 0,
57
- borderBottom: '1px solid var(--separator-primary)',
58
- }}
59
- >
60
- <View
61
- as={href ? 'a' : undefined}
62
- gap="m"
63
- layout="row"
64
- alignItems="center"
65
- style={{ flexGrow: 1, padding: '12px 0 12px 20px' }}
66
- href={href}
67
- onClick={onClick}
68
- >
69
- <ResourceIcon {...resource} />
70
- <Text>{typeof resource.name === 'string' && resource.name}</Text>
71
- <View style={{ flexGrow: 1 }} />
72
- <Size {...resource} />
73
- </View>
74
-
75
- <View style={{ padding: '12px 8px 12px 0' }}>
76
- <RowActions {...resource} />
77
- </View>
78
- </View>
79
- </Container>
80
- )
81
- }
82
-
83
- function Size (resource) {
84
- if (resource.content?.ContentLength) {
85
- return (
86
- <Text variant="small">
87
- {formatBytes(resource.content.ContentLength)}
88
- </Text>
89
- )
90
- }
91
-
92
- return <></>
93
- }
94
-
95
- function ResourceIcon ({ ...resource }) {
96
- const { workspace } = useWorkspace()
97
-
98
- const resourceTemplates = (workspace.resourceTemplates || [])
99
- .reduce((acc, curr) => ({ ...acc, [curr.id]: curr }), {})
100
-
101
- if (resource?.type === 'directory') {
102
- return (
103
- <Icon2 size="s" name="folder" style={{ fill: 'hsl(0, 0%, 60%)' }} />
104
- )
105
- }
106
-
107
- if (resource.type.startsWith('image')) {
108
- return (
109
- <Image
110
- src={resource?.content?.sizes?.thumbnailSmall || resource?.content?.src}
111
- placeholderSrc={resource?.content?.sizes?.['loader-square-blurred-after'] || resource?.content?.src}
112
- style={{ width: '24px', height: '24px', borderRadius: '25%' }}
113
- />
114
- )
115
- }
116
-
117
- return (
118
- <Icon2
119
- size="s"
120
- name={resourceTemplates[resource.type]?.icon || 'file'}
121
- style={{ fill: 'hsl(0, 0%, 80%)' }}
122
- />
123
- )
124
- }
125
-
126
- function RowActions ({ actions }) {
127
- if (!actions) return <></>
128
-
129
- return (
130
- <Dropdown trigger={<Button prefix="more-vertical-alt" variant="command" />}>
131
- <View inset="xs" surface="primary" roundness="s">
132
- <ContextMenu roundness="s" surface="primary">
133
- {actions}
134
- </ContextMenu>
135
- </View>
136
- </Dropdown>
137
- )
138
- }
package/src/constants.js DELETED
@@ -1,2 +0,0 @@
1
- export const PLATFORM_RESOURCE_LOCATION = '/@ossy/platform/'
2
- export const PLATFORM_CONFIG_TEMPLATE_ID = '@ossy/platform/config'
package/src/index.js DELETED
@@ -1,6 +0,0 @@
1
- export { Definition } from './Definition.js'
2
- export { PlatformResourceTemplates } from './resourceTemplates.js'
3
- export { PLATFORM_RESOURCE_LOCATION, PLATFORM_CONFIG_TEMPLATE_ID } from './constants.js'
4
- export { platformHomeMetadata } from './platformHomeMetadata.js'
5
- export { PlatformHomeBody } from './PlatformHomeBody.jsx'
6
- export { moduleStatusTags, moduleStatuses, MODULE_STATUS } from './moduleStatus.js'
@@ -1,34 +0,0 @@
1
- /**
2
- * Minimal subset of website module status helpers for {@link Definition} tags.
3
- */
4
-
5
- export const MODULE_STATUS = {
6
- beta: 'beta',
7
- comingSoon: 'coming-soon',
8
- stable: 'stable',
9
- hidden: 'hidden',
10
- }
11
-
12
- const STATUS_TO_TAG_LABEL = {
13
- [MODULE_STATUS.beta]: 'Beta',
14
- [MODULE_STATUS.comingSoon]: 'Coming Soon',
15
- [MODULE_STATUS.stable]: null,
16
- [MODULE_STATUS.hidden]: null,
17
- }
18
-
19
- export function moduleStatuses (definition) {
20
- const fromArray = Array.isArray(definition?.statuses) ? definition.statuses : []
21
- const fromLegacy = definition?.status && typeof definition.status === 'string' ? [definition.status] : []
22
- if (fromArray.length) return [...new Set(fromArray)]
23
- if (fromLegacy.length) return [...new Set(fromLegacy)]
24
- return []
25
- }
26
-
27
- export function moduleStatusTags (definition) {
28
- const labels = []
29
- for (const s of moduleStatuses(definition)) {
30
- const label = STATUS_TO_TAG_LABEL[s]
31
- if (label) labels.push(label)
32
- }
33
- return [...new Set(labels)]
34
- }
@@ -1,11 +0,0 @@
1
- import React from 'react'
2
- import { usePageShell } from '@ossy/connected-components'
3
- import { PlatformHomeBody } from '../PlatformHomeBody.jsx'
4
- import { platformHomeMetadata } from '../platformHomeMetadata.js'
5
-
6
- export const metadata = platformHomeMetadata
7
-
8
- export default function PlatformHomePage () {
9
- const Shell = usePageShell()
10
- return React.createElement(Shell, null, React.createElement(PlatformHomeBody))
11
- }
@@ -1,11 +0,0 @@
1
- import { Definition } from './Definition.js'
2
-
3
- export const platformHomeMetadata = {
4
- id: 'platform/home',
5
- title: Definition.title,
6
- description: Definition.description,
7
- path: {
8
- sv: '/plattform',
9
- en: '/platform',
10
- },
11
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * Matches the shape written to S3 as `platform-config.json` by deployment-tools
3
- * (`BucketDeployment` → `Source.jsonData('platform-config.json', { ...config, awsRoleToAssume: undefined })`).
4
- *
5
- * @see ossy/packages/deployment-tools/src/config/platform-config.js (PlatformConfig typedef)
6
- * @see ossy/packages/deployment-tools/src/infrastructure/container-deployment-target/container-deployment-target.js
7
- *
8
- * Note: `awsRoleToAssume` is intentionally omitted from the uploaded file (secret); do not store it here.
9
- * Optional `sesDomains` / `dnsRecords` are JSON text fields for arrays/objects.
10
- */
11
- export const PlatformResourceTemplates = [
12
- {
13
- name: 'Platform config',
14
- id: '@ossy/platform/config',
15
- icon: 'controller',
16
- fields: [
17
- {
18
- name: 'platformId',
19
- label: 'Platform ID (Ossy reference)',
20
- type: 'text',
21
- },
22
- {
23
- name: 'platformName',
24
- label: 'platformName',
25
- type: 'text',
26
- },
27
- {
28
- name: 'awsAccountId',
29
- label: 'awsAccountId',
30
- type: 'text',
31
- },
32
- {
33
- name: 'awsRegion',
34
- label: 'awsRegion',
35
- type: 'text',
36
- },
37
- {
38
- name: 'awsKeyPairName',
39
- label: 'awsKeyPairName',
40
- type: 'text',
41
- },
42
- {
43
- name: 'awsStaticBucketName',
44
- label: 'awsStaticBucketName',
45
- type: 'text',
46
- },
47
- {
48
- name: 'awsDeploymentSqsName',
49
- label: 'awsDeploymentSqsName',
50
- type: 'text',
51
- },
52
- {
53
- name: 'awsDeploymentSqsArn',
54
- label: 'awsDeploymentSqsArn',
55
- type: 'text',
56
- },
57
- {
58
- name: 'ciGithubActionsRepo',
59
- label: 'ciGithubActionsRepo (org/repo)',
60
- type: 'text',
61
- },
62
- {
63
- name: 'ciDockerNetworkName',
64
- label: 'ciDockerNetworkName',
65
- type: 'text',
66
- },
67
- {
68
- name: 'sesDomains',
69
- label: 'sesDomains (JSON array of strings, optional)',
70
- type: 'textarea',
71
- },
72
- {
73
- name: 'dnsRecords',
74
- label: 'dnsRecords (JSON object, optional)',
75
- type: 'textarea',
76
- },
77
- {
78
- name: 'Notes',
79
- label: 'Notes',
80
- type: 'textarea',
81
- },
82
- ],
83
- },
84
- ]
@@ -1,14 +0,0 @@
1
- /**
2
- * Converts bytes to a human-readable format.
3
- * @param {number} bytes - The number of bytes.
4
- * @param {number} decimals - The number of decimal places to include (default is 2).
5
- * @returns {string} - The human-readable format.
6
- */
7
- export function formatBytes (bytes, decimals = 2) {
8
- if (!bytes) return
9
- const k = 1024
10
- const dm = decimals < 0 ? 0 : decimals
11
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
12
- const i = Math.floor(Math.log(bytes) / Math.log(k))
13
- return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
14
- }