@ossy/platform 0.0.1-alpha.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
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.1 (2026-03-29)
7
+
8
+ **Note:** Version bump only for package @ossy/platform
9
+
10
+
11
+
12
+
13
+
14
+ # Changelog
15
+
16
+ ## 0.0.1-alpha.0
17
+
18
+ - Initial extract from `website-ossy`: `Definition`, `PlatformResourceTemplates`, `PlatformHomeBody`, `platformHomeMetadata`.
package/README.md ADDED
@@ -0,0 +1,30 @@
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/design-system`
15
+ - `@ossy/router-react`
16
+ - `@ossy/sdk-react`
17
+
18
+ ## Use in a website
19
+
20
+ 1. Spread **`PlatformResourceTemplates`** into your app `resourceTemplates` (and sync to the API via publish / cms upload).
21
+ 2. Add a page that wraps **`PlatformHomeBody`** in your shell layout and export **`platformHomeMetadata`**.
22
+ 3. Register **`Definition`** in your navigation / module registry.
23
+
24
+ ## Publishing
25
+
26
+ Released on its **own semver** from this monorepo (`lerna publish` with independent versions). Bump **`@ossy/platform`** when only this module changes.
27
+
28
+ ## Direction
29
+
30
+ 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/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@ossy/platform",
3
+ "version": "0.0.1-alpha.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/ossy-se/ossy.git",
7
+ "directory": "packages/platform"
8
+ },
9
+ "type": "module",
10
+ "description": "Platform module: deployment platform config resource template and home UI for Ossy websites",
11
+ "exports": {
12
+ ".": "./src/index.js",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "main": "./src/index.js",
16
+ "sideEffects": false,
17
+ "scripts": {
18
+ "build": "echo \"@ossy/platform: no build step\" && exit 0",
19
+ "test": ""
20
+ },
21
+ "author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
22
+ "license": "MIT",
23
+ "files": [
24
+ "src",
25
+ "README.md",
26
+ "CHANGELOG.md"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "registry": "https://registry.npmjs.org"
31
+ },
32
+ "peerDependencies": {
33
+ "@ossy/design-system": ">=0.14.0",
34
+ "@ossy/router-react": ">=0.14.0",
35
+ "@ossy/sdk-react": ">=0.14.0",
36
+ "react": ">=19.0.0"
37
+ },
38
+ "gitHead": "1da35d94f324f917d5ea790f838ce8e50214730e"
39
+ }
@@ -0,0 +1,13 @@
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
+ }
@@ -0,0 +1,88 @@
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
+ }
@@ -0,0 +1,138 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ export const PLATFORM_RESOURCE_LOCATION = '/@ossy/platform/'
2
+ export const PLATFORM_CONFIG_TEMPLATE_ID = '@ossy/platform/config'
package/src/index.js ADDED
@@ -0,0 +1,6 @@
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'
@@ -0,0 +1,34 @@
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
+ }
@@ -0,0 +1,11 @@
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
+ }
@@ -0,0 +1,84 @@
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
+ ]
@@ -0,0 +1,14 @@
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
+ }