@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 +18 -0
- package/README.md +30 -0
- package/package.json +39 -0
- package/src/Definition.js +13 -0
- package/src/PlatformHomeBody.jsx +88 -0
- package/src/ResourceList.jsx +138 -0
- package/src/constants.js +2 -0
- package/src/index.js +6 -0
- package/src/moduleStatus.js +34 -0
- package/src/platformHomeMetadata.js +11 -0
- package/src/resourceTemplates.js +84 -0
- package/src/utils/format-bytes.js +14 -0
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
|
+
}
|
package/src/constants.js
ADDED
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,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
|
+
}
|