mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
update/alpha.9 (#2988)
* import marketplac preview for sideload * fix: improve state service (#2977) * fix: fix sideload DI * fix: update Angular * fix: cleanup * fix: fix version selection * Bump node version to fix build for Angular * misc fixes - update node to v22 - fix chroot-and-upgrade access to prune-images - don't self-migrate legacy packages - #2985 - move dataVersion to volume folder - remove "instructions.md" from s9pk - add "docsUrl" to manifest * version bump * include flavor when clicking view listing from updates tab * closes #2980 * fix: fix select button * bring back ssh keys * fix: drop 'portal' from all routes * fix: implement longtap action to select table rows * fix description for ssh page * replace instructions with docsLink and refactor marketplace preview * delete unused translations * fix patchdb diffing algorithm * continue refactor of marketplace lib show components * Booting StartOS instead of Setting up your server on init * misc fixes - closes #2990 - closes #2987 * fix build * docsUrl and clickable service headers * don't cleanup after update until new service install succeeds * update types * misc fixes * beta.35 * sdkversion, githash for sideload, correct logs for init, startos pubkey display * bring back reboot button on install * misc fixes * beta.36 * better handling of setup and init for websocket errors * reopen init and setup logs even on graceful closure * better logging, misc fixes * fix build * dont let package stats hang * dont show docsurl in marketplace if no docsurl * re-add needs-config * show error if init fails, shorten hover state on header icons * fix operator precedemce --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Mariusz Kogen <k0gen@pm.me>
This commit is contained in:
@@ -385,7 +385,29 @@
|
||||
"changeDetection": "OnPush",
|
||||
"style": "scss",
|
||||
"skipTests": true,
|
||||
"skipImport": true
|
||||
"skipImport": true,
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3821
web/package-lock.json
generated
3821
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.4.0-alpha.8",
|
||||
"version": "0.4.0-alpha.9",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"license": "MIT",
|
||||
@@ -30,34 +30,34 @@
|
||||
"build-config": "node build-config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.2.11",
|
||||
"@angular/cdk": "^19.2.16",
|
||||
"@angular/common": "^19.2.11",
|
||||
"@angular/compiler": "^19.2.11",
|
||||
"@angular/core": "^19.2.11",
|
||||
"@angular/forms": "^19.2.11",
|
||||
"@angular/platform-browser": "^19.2.11",
|
||||
"@angular/platform-browser-dynamic": "^19.2.11",
|
||||
"@angular/pwa": "^19.2.12",
|
||||
"@angular/router": "^19.2.11",
|
||||
"@angular/service-worker": "^19.2.11",
|
||||
"@angular/animations": "^20.1.0",
|
||||
"@angular/cdk": "^20.1.0",
|
||||
"@angular/common": "^20.1.0",
|
||||
"@angular/compiler": "^20.1.0",
|
||||
"@angular/core": "^20.1.0",
|
||||
"@angular/forms": "^20.1.0",
|
||||
"@angular/platform-browser": "^20.1.0",
|
||||
"@angular/platform-browser-dynamic": "^20.1.0",
|
||||
"@angular/pwa": "^20.1.0",
|
||||
"@angular/router": "^20.1.0",
|
||||
"@angular/service-worker": "^20.1.0",
|
||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@start9labs/argon2": "^0.3.0",
|
||||
"@start9labs/start-sdk": "file:../sdk/baseDist",
|
||||
"@taiga-ui/addon-charts": "4.41.0",
|
||||
"@taiga-ui/addon-commerce": "4.41.0",
|
||||
"@taiga-ui/addon-mobile": "4.41.0",
|
||||
"@taiga-ui/addon-table": "4.41.0",
|
||||
"@taiga-ui/cdk": "4.41.0",
|
||||
"@taiga-ui/core": "4.41.0",
|
||||
"@taiga-ui/addon-charts": "4.44.0",
|
||||
"@taiga-ui/addon-commerce": "4.44.0",
|
||||
"@taiga-ui/addon-mobile": "4.44.0",
|
||||
"@taiga-ui/addon-table": "4.44.0",
|
||||
"@taiga-ui/cdk": "4.44.0",
|
||||
"@taiga-ui/core": "4.44.0",
|
||||
"@taiga-ui/event-plugins": "4.6.0",
|
||||
"@taiga-ui/experimental": "4.41.0",
|
||||
"@taiga-ui/icons": "4.41.0",
|
||||
"@taiga-ui/kit": "4.41.0",
|
||||
"@taiga-ui/layout": "4.41.0",
|
||||
"@taiga-ui/legacy": "4.41.0",
|
||||
"@taiga-ui/experimental": "4.44.0",
|
||||
"@taiga-ui/icons": "4.44.0",
|
||||
"@taiga-ui/kit": "4.44.0",
|
||||
"@taiga-ui/layout": "4.44.0",
|
||||
"@taiga-ui/legacy": "4.44.0",
|
||||
"@taiga-ui/polymorpheus": "4.9.0",
|
||||
"@taiga-ui/dompurify": "4.1.11",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
@@ -78,7 +78,7 @@
|
||||
"mime": "^4.0.3",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"mustache": "^4.2.0",
|
||||
"ng-qrcode": "^19.0.1",
|
||||
"ng-qrcode": "^20.0.0",
|
||||
"node-jose": "^2.2.0",
|
||||
"patch-db-client": "file:../patch-db/client",
|
||||
"pbkdf2": "^3.1.2",
|
||||
@@ -89,10 +89,10 @@
|
||||
"zone.js": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^19.2.12",
|
||||
"@angular/cli": "^19.2.12",
|
||||
"@angular/compiler-cli": "^19.2.11",
|
||||
"@angular/language-service": "^19.2.11",
|
||||
"@angular/build": "^20.1.0",
|
||||
"@angular/cli": "^20.1.0",
|
||||
"@angular/compiler-cli": "^20.1.0",
|
||||
"@angular/language-service": "^20.1.0",
|
||||
"@angular-experts/hawkeye": "^1.7.2",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/estree": "^0.0.51",
|
||||
@@ -105,7 +105,7 @@
|
||||
"@types/uuid": "^8.3.1",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^13.2.0",
|
||||
"ng-packagr": "^19.2.2",
|
||||
"ng-packagr": "^20.1.0",
|
||||
"node-html-parser": "^5.3.3",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^3.5.3",
|
||||
|
||||
@@ -5,6 +5,5 @@
|
||||
"https://community-registry.start9.com/": "Community Registry"
|
||||
},
|
||||
"startosRegistry": "https://registry.start9.com/",
|
||||
"snakeHighScore": 0,
|
||||
"ackInstructions": {}
|
||||
"snakeHighScore": 0
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { TuiConfirmData } from '@taiga-ui/kit'
|
||||
export const SUCCESS: Partial<TuiDialogOptions<any>> = {
|
||||
label: 'Install Success',
|
||||
closeable: false,
|
||||
dismissible: false,
|
||||
size: 's',
|
||||
data: { button: 'Reboot' },
|
||||
}
|
||||
|
||||
@@ -4,20 +4,20 @@ import { knownRegistries, sameUrl } from '@start9labs/shared'
|
||||
@Component({
|
||||
selector: 'store-icon',
|
||||
template: `
|
||||
<img
|
||||
*ngIf="icon; else noIcon"
|
||||
[style.border-radius.%]="100"
|
||||
[style.max-width]="size || '100%'"
|
||||
[src]="icon"
|
||||
alt="Registry Icon"
|
||||
/>
|
||||
<ng-template #noIcon>
|
||||
@if (icon) {
|
||||
<img
|
||||
[style.border-radius.%]="100"
|
||||
[style.max-width]="size || '100%'"
|
||||
[src]="icon"
|
||||
alt="Registry Icon"
|
||||
/>
|
||||
} @else {
|
||||
<img
|
||||
[style.max-width]="size || '100%'"
|
||||
src="assets/img/storefront-outline.png"
|
||||
alt="Registry Icon"
|
||||
/>
|
||||
</ng-template>
|
||||
}
|
||||
`,
|
||||
styles: ':host { overflow: hidden; }',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { Exver, MarkdownPipe } from '@start9labs/shared'
|
||||
import { TuiDialogContext } from '@taiga-ui/core'
|
||||
import { NgDompurifyPipe } from '@taiga-ui/dompurify'
|
||||
import { TuiAccordion } from '@taiga-ui/kit'
|
||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { MarketplacePkg } from '../../src/types'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<tui-accordion>
|
||||
@for (note of notes | keyvalue: asIsOrder; track $index) {
|
||||
<tui-accordion-item>
|
||||
{{ note.key }}
|
||||
<ng-template tuiAccordionItemContent>
|
||||
<div [innerHTML]="note.value | markdown | dompurify"></div>
|
||||
</ng-template>
|
||||
</tui-accordion-item>
|
||||
}
|
||||
</tui-accordion>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, TuiAccordion, MarkdownPipe, NgDompurifyPipe],
|
||||
})
|
||||
export class ReleaseNotesComponent {
|
||||
private readonly exver = inject(Exver)
|
||||
private readonly pkg =
|
||||
injectContext<TuiDialogContext<void, MarketplacePkg>>().data
|
||||
|
||||
readonly notes = Object.entries(this.pkg.otherVersions)
|
||||
.filter(
|
||||
([v, _]) =>
|
||||
this.exver.getFlavor(v) === this.pkg.flavor &&
|
||||
this.exver.compareExver(this.pkg.version, v) === 1,
|
||||
)
|
||||
.reduce(
|
||||
(obj, [version, info]) => ({
|
||||
...obj,
|
||||
[version]: info.releaseNotes,
|
||||
}),
|
||||
{
|
||||
[`${this.pkg.version} (current)`]: this.pkg.releaseNotes,
|
||||
},
|
||||
)
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
export const RELEASE_NOTES = new PolymorpheusComponent(ReleaseNotesComponent)
|
||||
@@ -16,8 +16,8 @@
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -50;
|
||||
border-radius: 1.5rem;
|
||||
background-color: rgb(39 39 42);
|
||||
|
||||
139
web/projects/marketplace/src/pages/show/about.component.ts
Normal file
139
web/projects/marketplace/src/pages/show/about.component.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core'
|
||||
import { MarketplacePkgBase } from '../../types'
|
||||
import { CopyService } from '@start9labs/shared'
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { MarketplaceItemComponent } from './item.component'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-about',
|
||||
template: `
|
||||
<div class="background-border box-shadow-lg shadow-color-light">
|
||||
<div class="box-container">
|
||||
<div class="detail-container">
|
||||
<!-- version -->
|
||||
<marketplace-item
|
||||
[style.pointer-events]="'none'"
|
||||
[data]="pkg().version"
|
||||
label="Version"
|
||||
icon=""
|
||||
/>
|
||||
<!-- release date -->
|
||||
@if (pkg().s9pk?.publishedAt; as published) {
|
||||
<marketplace-item
|
||||
[style.pointer-events]="'none'"
|
||||
[data]="(published | date: 'medium')!"
|
||||
label="Released"
|
||||
icon=""
|
||||
/>
|
||||
}
|
||||
<!-- SDK version -->
|
||||
<marketplace-item
|
||||
[style.pointer-events]="'none'"
|
||||
[data]="pkg().sdkVersion || 'Unknown'"
|
||||
label="SDK Version"
|
||||
icon=""
|
||||
/>
|
||||
<!-- git hash -->
|
||||
@if (pkg().gitHash; as gitHash) {
|
||||
<marketplace-item
|
||||
(click)="copyService.copy(gitHash)"
|
||||
[data]="gitHash"
|
||||
label="Git Hash"
|
||||
icon="@tui.copy"
|
||||
class="item-copy"
|
||||
/>
|
||||
} @else {
|
||||
<div class="item-padding">
|
||||
<label tuiTitle>
|
||||
<span tuiSubtitle>Git Hash</span>
|
||||
Unknown
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<!-- license -->
|
||||
<marketplace-item
|
||||
(click)="static.emit('license')"
|
||||
[data]="pkg().license"
|
||||
label="License"
|
||||
icon="@tui.chevron-right"
|
||||
class="item-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="background-border box-shadow-lg shadow-color-light">
|
||||
<div class="box-container">
|
||||
<p [innerHTML]="pkg().description.long"></p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
.box-container {
|
||||
background-color: rgb(39 39 42);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem 1.75rem;
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
|
||||
& > * + * {
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 0;
|
||||
border-color: rgb(113 113 122);
|
||||
}
|
||||
}
|
||||
|
||||
.item-pointer:hover {
|
||||
cursor: pointer;
|
||||
|
||||
::ng-deep label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.item-copy:hover {
|
||||
cursor: copy;
|
||||
|
||||
::ng-deep label {
|
||||
cursor: copy;
|
||||
}
|
||||
}
|
||||
|
||||
.item-padding {
|
||||
padding: 0.75rem 0.25rem;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: rgb(var(--tw-color-gray-200) / 1);
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [MarketplaceItemComponent, DatePipe],
|
||||
})
|
||||
export class MarketplaceAboutComponent {
|
||||
readonly copyService = inject(CopyService)
|
||||
|
||||
readonly pkg = input.required<MarketplacePkgBase>()
|
||||
|
||||
readonly static = output<'license'>()
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<div class="background-border box-shadow-lg shadow-color-light">
|
||||
<div class="box-container">
|
||||
<h2 class="additional-detail-title">New in {{ pkg.version }}</h2>
|
||||
<p [innerHTML]="pkg.releaseNotes | markdown | dompurify"></p>
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
appearance="whiteblock"
|
||||
iconEnd="@tui.chevron-right"
|
||||
(click)="onPast()"
|
||||
>
|
||||
Past Release Notes
|
||||
</button>
|
||||
<h2 class="additional-detail-title" [style.margin-top.rem]="2">About</h2>
|
||||
<p [innerHTML]="pkg.description.long"></p>
|
||||
<a
|
||||
*ngIf="pkg.marketingSite as url"
|
||||
tuiButton
|
||||
iconEnd="@tui.external-link"
|
||||
size="s"
|
||||
appearance="secondary-grayscale"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
[href]="url"
|
||||
>
|
||||
View website
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
.box-container {
|
||||
background-color: rgb(39 39 42);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.75rem;
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { RELEASE_NOTES } from '../../../modals/release-notes.component'
|
||||
import { MarketplacePkgBase } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-about',
|
||||
templateUrl: 'about.component.html',
|
||||
styleUrls: ['about.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class AboutComponent {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkgBase
|
||||
|
||||
async onPast() {
|
||||
this.dialogs
|
||||
.open(RELEASE_NOTES, { label: 'Past Release Notes', data: this.pkg })
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { MarkdownPipe, SafeLinksDirective } from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { NgDompurifyPipe } from '@taiga-ui/dompurify'
|
||||
import { TuiTagModule } from '@taiga-ui/legacy'
|
||||
import { AboutComponent } from './about.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
TuiTagModule,
|
||||
NgDompurifyPipe,
|
||||
SafeLinksDirective,
|
||||
MarkdownPipe,
|
||||
TuiButton,
|
||||
],
|
||||
declarations: [AboutComponent],
|
||||
exports: [AboutComponent],
|
||||
})
|
||||
export class AboutModule {}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplaceAdditionalItemComponent } from './additional-item.component'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-additional-link',
|
||||
template: `
|
||||
<a [href]="url" target="_blank" rel="noreferrer">
|
||||
<marketplace-additional-item
|
||||
[label]="label"
|
||||
[icon]="icon"
|
||||
[data]="url"
|
||||
></marketplace-additional-item>
|
||||
</a>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, MarketplaceAdditionalItemComponent],
|
||||
})
|
||||
export class MarketplaceAdditionalLinkComponent {
|
||||
@Input({ required: true })
|
||||
label!: string
|
||||
|
||||
@Input({ required: true })
|
||||
icon!: string
|
||||
|
||||
@Input({ required: true })
|
||||
url!: string
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<div class="background-border shadow-color-light box-shadow-lg">
|
||||
<div class="box-container">
|
||||
<h2 class="additional-detail-title">Information</h2>
|
||||
<div class="detail-container">
|
||||
<!-- release date -->
|
||||
<marketplace-additional-item
|
||||
*ngIf="pkg.s9pk?.publishedAt as published"
|
||||
[data]="(published | date: 'medium')!"
|
||||
label="Released"
|
||||
icon=""
|
||||
/>
|
||||
<!-- git hash -->
|
||||
<marketplace-additional-item
|
||||
*ngIf="pkg.gitHash as gitHash; else noHash"
|
||||
(click)="copyService.copy(gitHash)"
|
||||
[data]="gitHash"
|
||||
label="Git Hash"
|
||||
icon="@tui.copy"
|
||||
class="item-copy"
|
||||
/>
|
||||
<ng-template #noHash>
|
||||
<div class="item-padding">
|
||||
<label tuiTitle>
|
||||
<span tuiSubtitle>Git Hash</span>
|
||||
Unknown
|
||||
</label>
|
||||
</div>
|
||||
</ng-template>
|
||||
<!-- license -->
|
||||
<marketplace-additional-item
|
||||
(click)="static.emit('license')"
|
||||
[data]="pkg.license"
|
||||
label="License"
|
||||
icon="@tui.chevron-right"
|
||||
class="item-pointer"
|
||||
/>
|
||||
<!-- instructions -->
|
||||
<marketplace-additional-item
|
||||
(click)="static.emit('instructions')"
|
||||
data="Click to view instructions"
|
||||
label="Instructions"
|
||||
icon="@tui.chevron-right"
|
||||
class="item-pointer"
|
||||
/>
|
||||
<!-- versions -->
|
||||
<ng-content />
|
||||
<!-- links -->
|
||||
<marketplace-additional-link
|
||||
*ngIf="pkg.marketingSite"
|
||||
[url]="pkg.marketingSite"
|
||||
label="Marketing Site"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
<marketplace-additional-link
|
||||
*ngIf="pkg.upstreamRepo"
|
||||
[url]="pkg.upstreamRepo"
|
||||
label="Source Repository"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
<marketplace-additional-link
|
||||
*ngIf="pkg.wrapperRepo"
|
||||
[url]="pkg.wrapperRepo"
|
||||
label="Wrapper Repository"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
<marketplace-additional-link
|
||||
*ngIf="pkg.supportSite"
|
||||
[url]="pkg.supportSite"
|
||||
label="Support Site"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,46 +0,0 @@
|
||||
.box-container {
|
||||
background-color: rgb(39 39 42);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.75rem;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
|
||||
& > * + * {
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 0;
|
||||
border-color: rgb(113 113 122);
|
||||
}
|
||||
}
|
||||
|
||||
.item-pointer:hover {
|
||||
cursor: pointer;
|
||||
|
||||
::ng-deep label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.item-copy:hover {
|
||||
cursor: copy;
|
||||
|
||||
::ng-deep label {
|
||||
cursor: copy;
|
||||
}
|
||||
}
|
||||
|
||||
.item-padding {
|
||||
padding: 0.75rem 0.25rem;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: rgb(var(--tw-color-gray-200) / 1);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { CopyService } from '@start9labs/shared'
|
||||
import { MarketplacePkgBase } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-additional',
|
||||
templateUrl: 'additional.component.html',
|
||||
styleUrls: ['additional.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class AdditionalComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkgBase
|
||||
|
||||
@Output()
|
||||
readonly static = new EventEmitter<'license' | 'instructions'>()
|
||||
|
||||
constructor(
|
||||
readonly copyService: CopyService,
|
||||
private readonly route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
readonly url = this.route.snapshot.queryParamMap.get('url') || undefined
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { AdditionalComponent } from './additional.component'
|
||||
import { TuiButton, TuiLabel, TuiTitle } from '@taiga-ui/core'
|
||||
import { MarketplaceAdditionalItemComponent } from './additional-item.component'
|
||||
import { MarketplaceAdditionalLinkComponent } from './additional-link.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
TuiButton,
|
||||
TuiLabel,
|
||||
MarketplaceAdditionalItemComponent,
|
||||
MarketplaceAdditionalLinkComponent,
|
||||
TuiTitle,
|
||||
],
|
||||
declarations: [AdditionalComponent],
|
||||
exports: [AdditionalComponent],
|
||||
})
|
||||
export class AdditionalModule {}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CommonModule, KeyValue } from '@angular/common'
|
||||
import { KeyValue } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { ExverPipesModule } from '@start9labs/shared'
|
||||
@@ -19,10 +19,11 @@ import { MarketplacePkgBase } from '../../../types'
|
||||
{{ getTitle(dep.key) }}
|
||||
</span>
|
||||
<p>
|
||||
<ng-container [ngSwitch]="dep.value.optional">
|
||||
<span *ngSwitchCase="true">(optional)</span>
|
||||
<span *ngSwitchCase="false">(required)</span>
|
||||
</ng-container>
|
||||
@if (dep.value.optional) {
|
||||
<span>(optional)</span>
|
||||
} @else {
|
||||
<span>(required)</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -87,13 +88,7 @@ import { MarketplacePkgBase } from '../../../types'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
TuiAvatar,
|
||||
ExverPipesModule,
|
||||
TuiLineClamp,
|
||||
],
|
||||
imports: [RouterModule, TuiAvatar, ExverPipesModule, TuiLineClamp],
|
||||
})
|
||||
export class MarketplaceDepItemComponent {
|
||||
@Input({ required: true })
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiAvatar } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { MarketplacePkg } from '../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-flavors',
|
||||
@@ -33,7 +33,7 @@ import { MarketplacePkg } from '../../../types'
|
||||
.box-container {
|
||||
background-color: rgb(39 39 42);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.75rem;
|
||||
padding: 1.25rem 1.75rem;
|
||||
}
|
||||
|
||||
[tuiCell] {
|
||||
@@ -44,7 +44,7 @@ import { MarketplacePkg } from '../../../types'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [RouterLink, TuiCell, TuiTitle, SharedPipesModule, TuiAvatar],
|
||||
})
|
||||
export class FlavorsComponent {
|
||||
export class MarketplaceFlavorsComponent {
|
||||
@Input()
|
||||
pkgs!: MarketplacePkg[]
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
||||
|
||||
@@ -17,8 +16,6 @@ import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
||||
<div class="dark-overlay"></div>
|
||||
<div class="inner-container-title">
|
||||
<h2 ticker>{{ pkg.title }}</h2>
|
||||
<h3>{{ pkg.version }}</h3>
|
||||
<p>{{ pkg.description.short }}</p>
|
||||
</div>
|
||||
<!-- control buttons -->
|
||||
<ng-content />
|
||||
@@ -43,7 +40,7 @@ import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
||||
min-height: 32vh;
|
||||
position: relative;
|
||||
border-radius: 1.5rem;
|
||||
padding: 4rem 2rem 0 2rem;
|
||||
padding: 3rem 2rem 0 2rem;
|
||||
|
||||
@media (min-width: 376px) {
|
||||
min-height: 20vh;
|
||||
@@ -66,7 +63,7 @@ import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
||||
}
|
||||
|
||||
.inner-container-title {
|
||||
margin: 1rem 0;
|
||||
margin: 0.5rem 0 1rem 0;
|
||||
color: rgb(250 250 250);
|
||||
mix-blend-mode: plus-lighter;
|
||||
z-index: 1;
|
||||
@@ -77,29 +74,11 @@ import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
font-size: 2.5rem;
|
||||
line-height: 3rem;
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: normal;
|
||||
margin-bottom: 1rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
font-weight: 300;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
margin: 1rem 0 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +117,7 @@ import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, SharedPipesModule, TickerComponent],
|
||||
imports: [SharedPipesModule, TickerComponent],
|
||||
})
|
||||
export class MarketplacePackageHeroComponent {
|
||||
@Input({ required: true })
|
||||
@@ -1,10 +1,9 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiFade } from '@taiga-ui/kit'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-additional-item',
|
||||
selector: 'marketplace-item',
|
||||
template: `
|
||||
<label tuiTitle>
|
||||
<span tuiSubtitle>{{ label }}</span>
|
||||
@@ -35,9 +34,9 @@ import { TuiFade } from '@taiga-ui/kit'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, TuiIcon, TuiTitle, TuiFade],
|
||||
imports: [TuiIcon, TuiTitle, TuiFade],
|
||||
})
|
||||
export class MarketplaceAdditionalItemComponent {
|
||||
export class MarketplaceItemComponent {
|
||||
@Input({ required: true })
|
||||
label!: string
|
||||
|
||||
23
web/projects/marketplace/src/pages/show/link.component.ts
Normal file
23
web/projects/marketplace/src/pages/show/link.component.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplaceItemComponent } from './item.component'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-link',
|
||||
template: `
|
||||
<a [href]="url" target="_blank" rel="noreferrer">
|
||||
<marketplace-item [label]="label" [icon]="icon" [data]="url" />
|
||||
</a>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [MarketplaceItemComponent],
|
||||
})
|
||||
export class MarketplaceLinkComponent {
|
||||
@Input({ required: true })
|
||||
label!: string
|
||||
|
||||
@Input({ required: true })
|
||||
icon!: string
|
||||
|
||||
@Input({ required: true })
|
||||
url!: string
|
||||
}
|
||||
128
web/projects/marketplace/src/pages/show/links.component.ts
Normal file
128
web/projects/marketplace/src/pages/show/links.component.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { CopyService } from '@start9labs/shared'
|
||||
import { MarketplacePkgBase } from '../../types'
|
||||
import { MarketplaceLinkComponent } from './link.component'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-links',
|
||||
template: `
|
||||
<div class="background-border shadow-color-light box-shadow-lg">
|
||||
<div class="box-container">
|
||||
<h2 class="additional-detail-title">Source Code</h2>
|
||||
<div class="detail-container">
|
||||
<marketplace-link
|
||||
[url]="pkg().upstreamRepo"
|
||||
label="Upstream Service"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
<marketplace-link
|
||||
[url]="pkg().wrapperRepo"
|
||||
label="StartOS Package"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="background-border shadow-color-light box-shadow-lg">
|
||||
<div class="box-container">
|
||||
<h2 class="additional-detail-title">Links</h2>
|
||||
<div class="detail-container">
|
||||
<marketplace-link
|
||||
[url]="pkg().marketingSite"
|
||||
label="Marketing"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
@if (pkg().docsUrl; as docsUrl) {
|
||||
<marketplace-link
|
||||
[url]="docsUrl"
|
||||
label="Documentation"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
}
|
||||
<marketplace-link
|
||||
[url]="pkg().supportSite"
|
||||
label="Support"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
@if (pkg().donationUrl; as donationUrl) {
|
||||
<marketplace-link
|
||||
[url]="donationUrl"
|
||||
label="Donations"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
.box-container {
|
||||
background-color: rgb(39 39 42);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem 1.75rem;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
|
||||
& > * + * {
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 0;
|
||||
border-color: rgb(113 113 122);
|
||||
}
|
||||
}
|
||||
|
||||
.item-pointer:hover {
|
||||
cursor: pointer;
|
||||
|
||||
::ng-deep label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.item-copy:hover {
|
||||
cursor: copy;
|
||||
|
||||
::ng-deep label {
|
||||
cursor: copy;
|
||||
}
|
||||
}
|
||||
|
||||
.item-padding {
|
||||
padding: 0.75rem 0.25rem;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: rgb(var(--tw-color-gray-200) / 1);
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [MarketplaceLinkComponent],
|
||||
})
|
||||
export class MarketplaceLinksComponent {
|
||||
readonly copyService = inject(CopyService)
|
||||
readonly url =
|
||||
inject(ActivatedRoute).snapshot.queryParamMap.get('url') || undefined
|
||||
|
||||
readonly pkg = input.required<MarketplacePkgBase>()
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { MarkdownPipe } from '@start9labs/shared'
|
||||
import { NgDompurifyPipe } from '@taiga-ui/dompurify'
|
||||
import { MarketplacePkgBase } from '../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-release-notes',
|
||||
template: `
|
||||
<div class="background-border box-shadow-lg shadow-color-light">
|
||||
<div class="box-container">
|
||||
<h2 class="additional-detail-title">New in {{ pkg().version }}</h2>
|
||||
<p [innerHTML]="pkg().releaseNotes | markdown | dompurify"></p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
.box-container {
|
||||
background-color: rgb(39 39 42);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem 1.75rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgDompurifyPipe, MarkdownPipe],
|
||||
})
|
||||
export class MarketplaceReleaseNotesComponent {
|
||||
readonly pkg = input.required<MarketplacePkgBase>()
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { TuiCarousel } from '@taiga-ui/kit'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -7,73 +5,72 @@ import {
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||
import { TuiDialogContext, TuiDialogService, TuiButton } from '@taiga-ui/core'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { TuiButton, TuiDialogContext, TuiDialogService } from '@taiga-ui/core'
|
||||
import { TuiCarousel } from '@taiga-ui/kit'
|
||||
import { PolymorpheusContent } from '@taiga-ui/polymorpheus'
|
||||
import { MarketplacePkg } from '../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-package-screenshots',
|
||||
template: `
|
||||
<!--@TODO future release-->
|
||||
<div
|
||||
*ngIf="$any(pkg).screenshots as screenshots"
|
||||
tuiCarouselButtons
|
||||
class="outer-container"
|
||||
>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.chevron-left"
|
||||
title="Previous"
|
||||
type="button"
|
||||
(click)="carousel.prev()"
|
||||
></button>
|
||||
<tui-carousel
|
||||
#carousel
|
||||
[itemsCount]="isMobile ? 1 : 2"
|
||||
[(index)]="index"
|
||||
class="carousel"
|
||||
>
|
||||
<ng-container *ngFor="let item of screenshots; let i = index">
|
||||
<div
|
||||
*tuiItem
|
||||
draggable="false"
|
||||
[class.item_active]="i === index + 1"
|
||||
class="screenshot-item"
|
||||
>
|
||||
<img
|
||||
#template
|
||||
alt="Service screenshot"
|
||||
src="assets/img/temp/{{ item }}"
|
||||
class="screenshot-item-img"
|
||||
(click)="presentModalImg(dialogTemplate)"
|
||||
/>
|
||||
<ng-template #dialogTemplate let-observer>
|
||||
@if ($any(pkg).screenshots; as screenshots) {
|
||||
<div tuiCarouselButtons class="outer-container">
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
iconStart="@tui.chevron-left"
|
||||
title="Previous"
|
||||
type="button"
|
||||
(click)="carousel.prev()"
|
||||
></button>
|
||||
<tui-carousel
|
||||
#carousel
|
||||
[itemsCount]="isMobile ? 1 : 2"
|
||||
[(index)]="index"
|
||||
class="carousel"
|
||||
>
|
||||
@for (item of screenshots; track item; let i = $index) {
|
||||
<div
|
||||
*tuiItem
|
||||
draggable="false"
|
||||
[class.item_active]="i === index + 1"
|
||||
class="screenshot-item"
|
||||
>
|
||||
<img
|
||||
#template
|
||||
alt="Service screenshot"
|
||||
src="assets/img/temp/{{ item }}"
|
||||
class="screenshot-item-img-enlarged"
|
||||
class="screenshot-item-img"
|
||||
(click)="presentModalImg(dialogTemplate)"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
</tui-carousel>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
type="button"
|
||||
iconStart="@tui.chevron-right"
|
||||
title="Next"
|
||||
(click)="carousel.next()"
|
||||
></button>
|
||||
</div>
|
||||
<ng-template #dialogTemplate let-observer>
|
||||
<img
|
||||
alt="Service screenshot"
|
||||
src="assets/img/temp/{{ item }}"
|
||||
class="screenshot-item-img-enlarged"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
}
|
||||
</tui-carousel>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="flat-grayscale"
|
||||
type="button"
|
||||
iconStart="@tui.chevron-right"
|
||||
title="Next"
|
||||
(click)="carousel.next()"
|
||||
></button>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
.outer-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
margin-left: -3.5rem;
|
||||
@@ -123,7 +120,7 @@ import { PolymorpheusContent } from '@taiga-ui/polymorpheus'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, TuiCarousel, TuiButton],
|
||||
imports: [TuiCarousel, TuiButton],
|
||||
})
|
||||
export class MarketplacePackageScreenshotComponent {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
102
web/projects/marketplace/src/pages/show/versions.component.ts
Normal file
102
web/projects/marketplace/src/pages/show/versions.component.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
TemplateRef,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { DialogService, i18nPipe, SharedPipesModule } from '@start9labs/shared'
|
||||
import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { TuiRadioList } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
import { MarketplaceItemComponent } from './item.component'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-versions',
|
||||
template: `
|
||||
<div class="background-border shadow-color-light box-shadow-lg">
|
||||
<div class="box-container">
|
||||
<h2 class="additional-detail-title">Versions</h2>
|
||||
<marketplace-item
|
||||
(click)="promptSelectVersion(versionSelect)"
|
||||
data="Select another version"
|
||||
icon="@tui.chevron-right"
|
||||
label=""
|
||||
class="select"
|
||||
/>
|
||||
<ng-template
|
||||
#versionSelect
|
||||
let-data="data"
|
||||
let-completeWith="completeWith"
|
||||
>
|
||||
<tui-radio-list [items]="versions()" [(ngModel)]="data.version" />
|
||||
<footer class="buttons">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
(click)="completeWith(null)"
|
||||
>
|
||||
{{ 'Cancel' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
(click)="completeWith(data.version)"
|
||||
>
|
||||
{{ 'Ok' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
.box-container {
|
||||
background-color: rgb(39 39 42);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem 1.75rem;
|
||||
}
|
||||
|
||||
.select {
|
||||
border: 0;
|
||||
// border-top-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
border-color: rgb(113 113 122);
|
||||
border-style: solid;
|
||||
cursor: pointer;
|
||||
|
||||
::ng-deep label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
MarketplaceItemComponent,
|
||||
TuiButton,
|
||||
SharedPipesModule,
|
||||
FormsModule,
|
||||
TuiRadioList,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export class MarketplaceVersionsComponent {
|
||||
private readonly dialog = inject(DialogService)
|
||||
readonly version = input.required<string | null>()
|
||||
readonly versions = input.required<string[]>()
|
||||
|
||||
onVersion = output<string>()
|
||||
|
||||
promptSelectVersion(template: TemplateRef<TuiDialogContext>) {
|
||||
this.dialog
|
||||
.openComponent<string>(template, {
|
||||
label: 'All versions',
|
||||
size: 's',
|
||||
data: { version: this.version() },
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(selected => this.onVersion.emit(selected))
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,17 @@ export * from './pages/list/item/item.component'
|
||||
export * from './pages/list/item/item.module'
|
||||
export * from './pages/list/search/search.component'
|
||||
export * from './pages/list/search/search.module'
|
||||
export * from './pages/show/about/about.component'
|
||||
export * from './pages/show/about/about.module'
|
||||
export * from './pages/show/additional/additional-link.component'
|
||||
export * from './pages/show/additional/additional-item.component'
|
||||
export * from './pages/show/additional/additional.component'
|
||||
export * from './pages/show/additional/additional.module'
|
||||
export * from './pages/show/link.component'
|
||||
export * from './pages/show/item.component'
|
||||
export * from './pages/show/links.component'
|
||||
export * from './pages/show/dependencies/dependencies.component'
|
||||
export * from './pages/show/dependencies/dependency-item.component'
|
||||
export * from './pages/show/screenshots/screenshots.component'
|
||||
export * from './pages/show/hero/hero.component'
|
||||
export * from './pages/show/flavors/flavors.component'
|
||||
export * from './pages/show/about.component'
|
||||
export * from './pages/show/screenshots.component'
|
||||
export * from './pages/show/hero.component'
|
||||
export * from './pages/show/flavors.component'
|
||||
export * from './pages/show/versions.component'
|
||||
export * from './pages/show/release-notes.component'
|
||||
|
||||
export * from './pipes/filter-packages.pipe'
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { Component, inject, DOCUMENT } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api.service'
|
||||
import { StateService } from './services/state.service'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
||||
@@ -65,8 +65,8 @@ import { DocsLinkDirective } from '@start9labs/shared'
|
||||
text-align: center;
|
||||
border-radius: clamp(2rem, 3rem, 4rem);
|
||||
cursor: pointer;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px,
|
||||
rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0 4px 6px -1px,
|
||||
rgba(0, 0, 0, 0.06) 0 2px 4px -1px;
|
||||
background: #6866cc;
|
||||
color: #f4f4f5;
|
||||
"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, OnInit } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
@@ -102,7 +101,6 @@ import { StateService } from 'src/app/services/state.service'
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
TuiCardLarge,
|
||||
TuiButton,
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
ErrorService,
|
||||
formatProgress,
|
||||
getErrorMessage,
|
||||
InitializingComponent,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
catchError,
|
||||
EMPTY,
|
||||
filter,
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@Component({
|
||||
template: '<app-initializing [setupType]="type" [progress]="progress()" />',
|
||||
template:
|
||||
'<app-initializing [setupType]="type" [progress]="progress()" [error]="error()" />',
|
||||
styles: `
|
||||
:host {
|
||||
max-width: unset;
|
||||
@@ -35,25 +39,15 @@ import { StateService } from 'src/app/services/state.service'
|
||||
})
|
||||
export default class LoadingPage {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
|
||||
readonly type = inject(StateService).setupType
|
||||
readonly router = inject(Router)
|
||||
readonly progress = toSignal(
|
||||
from(this.getStatus()).pipe(
|
||||
filter(Boolean),
|
||||
take(1),
|
||||
switchMap(({ guid, progress }) =>
|
||||
this.api.openWebsocket$<T.FullProgress>(guid).pipe(
|
||||
startWith(progress),
|
||||
catchError((_, watch$) =>
|
||||
interval(2000).pipe(
|
||||
switchMap(() => from(this.api.getStatus())),
|
||||
catchError(() => EMPTY),
|
||||
take(1),
|
||||
switchMap(() => watch$),
|
||||
),
|
||||
),
|
||||
tap(({ overall }) => {
|
||||
if (overall === true) {
|
||||
this.getStatus()
|
||||
@@ -62,29 +56,32 @@ export default class LoadingPage {
|
||||
),
|
||||
),
|
||||
map(formatProgress),
|
||||
catchError(e => {
|
||||
this.errorService.handleError(e)
|
||||
return EMPTY
|
||||
}),
|
||||
catchError((_, caught$) => timer(500).pipe(switchMap(() => caught$))),
|
||||
),
|
||||
{ initialValue: { total: 0, message: '' } },
|
||||
)
|
||||
|
||||
error = signal('')
|
||||
|
||||
private async getStatus(): Promise<{
|
||||
status: 'running'
|
||||
guid: string
|
||||
progress: T.FullProgress
|
||||
} | null> {
|
||||
const res = await this.api.getStatus()
|
||||
try {
|
||||
const res = await this.api.getStatus()
|
||||
|
||||
if (!res) {
|
||||
this.router.navigate(['home'])
|
||||
return null
|
||||
} else if (res.status === 'complete') {
|
||||
this.router.navigate(['success'])
|
||||
return null
|
||||
} else {
|
||||
return res
|
||||
if (!res) {
|
||||
this.router.navigate(['home'])
|
||||
} else if (res.status === 'complete') {
|
||||
this.router.navigate(['success'])
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.error.set(getErrorMessage(e))
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
inject,
|
||||
ViewChild,
|
||||
DOCUMENT,
|
||||
} from '@angular/core'
|
||||
import { DownloadHTMLService, ErrorService } from '@start9labs/shared'
|
||||
import { TuiButton, TuiIcon, TuiSurface } from '@taiga-ui/core'
|
||||
|
||||
@@ -23,7 +23,7 @@ export abstract class ApiService {
|
||||
abstract execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> // setup.execute
|
||||
abstract complete(): Promise<T.SetupResult> // setup.complete
|
||||
abstract exit(): Promise<void> // setup.exit
|
||||
abstract followServerLogs(): Promise<FollowLogsRes> // setup.logs.follow
|
||||
abstract initFollowLogs(): Promise<FollowLogsRes> // setup.logs.follow
|
||||
abstract openWebsocket$<T>(guid: string): Observable<T>
|
||||
|
||||
async encrypt(toEncrypt: string): Promise<T.EncryptedWire> {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { Inject, Injectable, DOCUMENT } from '@angular/core'
|
||||
import {
|
||||
DiskListResponse,
|
||||
encodeBase64,
|
||||
@@ -98,7 +97,7 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async followServerLogs(): Promise<FollowLogsRes> {
|
||||
async initFollowLogs(): Promise<FollowLogsRes> {
|
||||
return this.rpcRequest({ method: 'setup.logs.follow', params: {} })
|
||||
}
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async followServerLogs(): Promise<FollowLogsRes> {
|
||||
async initFollowLogs(): Promise<FollowLogsRes> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
startCursor: 'fakestartcursor',
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiProgress } from '@taiga-ui/kit'
|
||||
import { LogsWindowComponent } from './logs-window.component'
|
||||
import { i18nPipe } from '../i18n/i18n.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'app-initializing',
|
||||
template: `
|
||||
<section>
|
||||
<h1 [style.font-size.rem]="2" [style.margin-bottom.rem]="2">
|
||||
{{ 'Setting up your server' | i18n }}
|
||||
</h1>
|
||||
<div>
|
||||
{{ 'Progress' | i18n }}: {{ (progress.total * 100).toFixed(0) }}%
|
||||
</div>
|
||||
<progress
|
||||
tuiProgressBar
|
||||
[style.max-width.rem]="40"
|
||||
[style.margin]="'1rem auto'"
|
||||
[attr.value]="progress.total"
|
||||
></progress>
|
||||
<p [innerHTML]="progress.message || 'Finished'"></p>
|
||||
</section>
|
||||
<logs-window />
|
||||
`,
|
||||
styles: `
|
||||
section {
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin: 1.5rem;
|
||||
text-align: center;
|
||||
// @TODO Theme
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
--tui-background-neutral-1: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
logs-window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 18rem;
|
||||
padding: 1rem;
|
||||
margin: 0 1.5rem auto;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
border-radius: 2rem;
|
||||
// @TODO Theme
|
||||
background: #181818;
|
||||
}
|
||||
`,
|
||||
imports: [CommonModule, LogsWindowComponent, TuiProgress, i18nPipe],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InitializingComponent {
|
||||
@Input()
|
||||
progress: { total: number; message: string } = { total: 0, message: '' }
|
||||
|
||||
@Input()
|
||||
setupType?: 'fresh' | 'restore' | 'attach' | 'transfer'
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { TuiProgress } from '@taiga-ui/kit'
|
||||
import { LogsWindowComponent } from './logs-window.component'
|
||||
import { i18nPipe } from '../../i18n/i18n.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'app-initializing',
|
||||
template: `
|
||||
@if (error(); as err) {
|
||||
<section>
|
||||
<h1>{{ 'Error initializing server' | i18n }}</h1>
|
||||
<p>{{ err }}</p>
|
||||
</section>
|
||||
} @else {
|
||||
<section>
|
||||
<h1 [style.font-size.rem]="2" [style.margin-bottom.rem]="2">
|
||||
{{
|
||||
setupType()
|
||||
? ('Setting up your server' | i18n)
|
||||
: ('Booting StartOS' | i18n)
|
||||
}}
|
||||
</h1>
|
||||
<div>
|
||||
{{ 'Progress' | i18n }}: {{ (progress().total * 100).toFixed(0) }}%
|
||||
</div>
|
||||
<progress
|
||||
tuiProgressBar
|
||||
[style.max-width.rem]="40"
|
||||
[style.margin]="'1rem auto'"
|
||||
[attr.value]="progress().total"
|
||||
></progress>
|
||||
<p [innerHTML]="message()"></p>
|
||||
</section>
|
||||
}
|
||||
<logs-window />
|
||||
`,
|
||||
styles: `
|
||||
section {
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin: 1.5rem;
|
||||
text-align: center;
|
||||
// @TODO Theme
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
--tui-background-neutral-1: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
logs-window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 18rem;
|
||||
padding: 1rem;
|
||||
margin: 0 1.5rem auto;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
border-radius: 2rem;
|
||||
// @TODO Theme
|
||||
background: #181818;
|
||||
}
|
||||
`,
|
||||
imports: [LogsWindowComponent, TuiProgress, i18nPipe],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InitializingComponent {
|
||||
private readonly i18nPipe = inject(i18nPipe)
|
||||
|
||||
readonly progress = input<{ total: number; message: string }>({
|
||||
total: 0,
|
||||
message: '',
|
||||
})
|
||||
readonly setupType = input<
|
||||
'fresh' | 'restore' | 'attach' | 'transfer' | undefined
|
||||
>()
|
||||
readonly error = input<string>()
|
||||
|
||||
readonly message = computed(() => {
|
||||
return (
|
||||
this.progress().message ||
|
||||
(this.progress().total === 1
|
||||
? this.i18nPipe.transform('Finished')
|
||||
: '...')
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { Component, ElementRef, inject } from '@angular/core'
|
||||
import { Component, ElementRef, inject, input } from '@angular/core'
|
||||
import {
|
||||
INTERSECTION_ROOT,
|
||||
WaIntersectionObserver,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { WaMutationObserver } from '@ng-web-apis/mutation-observer'
|
||||
import { TuiScrollbar } from '@taiga-ui/core'
|
||||
import { NgDompurifyPipe } from '@taiga-ui/dompurify'
|
||||
import { SetupLogsService } from '../services/setup-logs.service'
|
||||
import { SetupLogsService } from '../../services/setup-logs.service'
|
||||
|
||||
@Component({
|
||||
selector: 'logs-window',
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
||||
@@ -14,7 +13,9 @@ import { i18nKey } from '../i18n/i18n.providers'
|
||||
@Component({
|
||||
template: `
|
||||
<p>{{ options.message }}</p>
|
||||
<p *ngIf="options.warning" class="warning">{{ options.warning }}</p>
|
||||
@if (options.warning) {
|
||||
<p class="warning">{{ options.warning }}</p>
|
||||
}
|
||||
<form (ngSubmit)="submit(value.trim())">
|
||||
<tui-input
|
||||
tuiAutoFocus
|
||||
@@ -24,7 +25,9 @@ import { i18nKey } from '../i18n/i18n.providers'
|
||||
[(ngModel)]="value"
|
||||
>
|
||||
{{ options.label }}
|
||||
<span *ngIf="options.required !== false && options.label">*</span>
|
||||
@if (options.required !== false && options.label) {
|
||||
<span>*</span>
|
||||
}
|
||||
<input
|
||||
tuiTextfieldLegacy
|
||||
[class.masked]="options.useMask && masked && value"
|
||||
@@ -74,7 +77,6 @@ import { i18nKey } from '../i18n/i18n.providers'
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
TuiInputModule,
|
||||
TuiButton,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Directive, inject } from '@angular/core'
|
||||
import { Directive, inject, DOCUMENT } from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import {
|
||||
MutationObserverService,
|
||||
|
||||
@@ -274,11 +274,9 @@ export default {
|
||||
273: 'Git-Hash',
|
||||
274: 'Lizenz',
|
||||
275: 'Installiert von',
|
||||
276: 'Dienst-Repository',
|
||||
277: 'Paket-Repository',
|
||||
278: 'Marketing-Website',
|
||||
279: 'Support-Website',
|
||||
280: 'Spendenlink',
|
||||
278: 'Marketing',
|
||||
279: 'Support',
|
||||
280: 'Spenden',
|
||||
281: 'Standardaktionen',
|
||||
282: 'Dienst neu bauen',
|
||||
283: 'Baut den Dienst-Container neu. Nur erforderlich, wenn ein Fehler in StartOS vorliegt.',
|
||||
@@ -287,7 +285,6 @@ export default {
|
||||
286: 'Dashboard',
|
||||
287: 'dashboard',
|
||||
288: 'aktionen',
|
||||
289: 'anleitungen',
|
||||
290: 'logs',
|
||||
291: 'über',
|
||||
292: 'Upload wird gestartet',
|
||||
@@ -485,7 +482,7 @@ export default {
|
||||
484: 'Laden Sie die Seite neu. Wenn das nicht funktioniert, beenden Sie Ihren Browser und öffnen Sie ihn erneut, um diese Seite erneut zu besuchen.',
|
||||
485: 'StartOS-Benutzeroberfläche',
|
||||
486: 'WiFi',
|
||||
487: 'Anleitungen',
|
||||
487: 'Dokumentation',
|
||||
488: 'spanisch',
|
||||
489: 'polnisch',
|
||||
490: 'deutsch',
|
||||
@@ -496,7 +493,7 @@ export default {
|
||||
495: 'Validierung',
|
||||
496: 'in Bearbeitung',
|
||||
497: 'abgeschlossen',
|
||||
498: 'Klicken Sie hier, um alle Versionen anzuzeigen',
|
||||
498: 'StartOS wird gestartet',
|
||||
499: 'Um loszulegen, besuche den Marktplatz und lade deinen ersten Dienst herunter',
|
||||
500: 'Marktplatz anzeigen',
|
||||
501: 'Willkommen bei',
|
||||
@@ -522,4 +519,13 @@ export default {
|
||||
521: 'Um das Problem zu beheben, siehe',
|
||||
522: 'SDK Version',
|
||||
523: 'Sicherungsbericht',
|
||||
524: 'Ausgewählte löschen',
|
||||
525: 'Keine schlüssel',
|
||||
526: 'Öffentlichen SSH-Schlüssel hinzufügen',
|
||||
527: 'Standardmäßig kannst du dich per SSH von jedem Gerät aus mit deinem Server verbinden, indem du dein Master-Passwort verwendest. Optional kannst du SSH-öffentliche Schlüssel hinzufügen, um bestimmten Geräten den Zugriff ohne Passworteingabe zu ermöglichen.',
|
||||
528: 'Quellcode',
|
||||
529: 'Upstream-Dienst',
|
||||
530: 'StartOS-Paket',
|
||||
531: 'Fehler beim Initialisieren des Servers',
|
||||
532: 'Abgeschlossen',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -273,20 +273,17 @@ export const ENGLISH = {
|
||||
'Git Hash': 273,
|
||||
'License': 274,
|
||||
'Installed From': 275,
|
||||
'Service Repository': 276,
|
||||
'Package Repository': 277,
|
||||
'Marketing Site': 278,
|
||||
'Support Site': 279,
|
||||
'Donation Link': 280,
|
||||
'Marketing': 278,
|
||||
'Support': 279,
|
||||
'Donations': 280,
|
||||
'Standard Actions': 281,
|
||||
'Rebuild Service': 282, // as in, rebuild a software container
|
||||
'Rebuilds the service container. Only necessary if there is a bug in StartOS': 283,
|
||||
'Uninstall': 284,
|
||||
'Uninstalls this service from StartOS and delete all data permanently.': 285,
|
||||
'Uninstalls this service from StartOS and deletes all data permanently.': 285,
|
||||
'Dashboard': 286,
|
||||
'dashboard': 287,
|
||||
'actions': 288,
|
||||
'instructions': 289,
|
||||
'logs': 290, // as in, "application logs"
|
||||
'about': 291, // as in, "about this server"
|
||||
'Starting upload': 292,
|
||||
@@ -484,7 +481,7 @@ export const ENGLISH = {
|
||||
'Refresh the page. If refreshing the page does not work, you may need to quit and re-open your browser, then revisit this page.': 484,
|
||||
'StartOS UI': 485,
|
||||
'WiFi': 486,
|
||||
'Instructions': 487,
|
||||
'Documentation': 487, // as in, a website to view documentation
|
||||
'spanish': 488,
|
||||
'polish': 489,
|
||||
'german': 490,
|
||||
@@ -495,7 +492,7 @@ export const ENGLISH = {
|
||||
'Validating': 495,
|
||||
'in progress': 496,
|
||||
'complete': 497,
|
||||
'Click to view all versions': 498,
|
||||
'Booting StartOS': 498,
|
||||
'To get started, visit the Marketplace and download your first service': 499,
|
||||
'View Marketplace': 500,
|
||||
'Welcome to': 501,
|
||||
@@ -521,4 +518,13 @@ export const ENGLISH = {
|
||||
'To resolve the issue, refer to': 521,
|
||||
'SDK Version': 522,
|
||||
'Backup Report': 523,
|
||||
'Delete selected': 524,
|
||||
'No keys': 525,
|
||||
'Add SSH Public Key': 526,
|
||||
'By default, you can SSH into your server from any device using your master password. Optionally add SSH public keys to grant specific devices access without needing to enter a password.': 527,
|
||||
'Source Code': 528,
|
||||
'Upstream service': 529, // as in, the URL of the source code for the original software
|
||||
'StartOS package': 530, // as in, the URL of the source code for the StartOS package
|
||||
'Error initializing server': 531,
|
||||
'Finished': 532, // an in, complete
|
||||
} as const
|
||||
|
||||
@@ -274,11 +274,9 @@ export default {
|
||||
273: 'Hash de Git',
|
||||
274: 'Licencia',
|
||||
275: 'Instalado desde',
|
||||
276: 'Repositorio del servicio',
|
||||
277: 'Repositorio del paquete',
|
||||
278: 'Sitio de marketing',
|
||||
279: 'Sitio de soporte',
|
||||
280: 'Enlace de donación',
|
||||
278: 'Marketing',
|
||||
279: 'Soporte',
|
||||
280: 'Donaciones',
|
||||
281: 'Acciones estándar',
|
||||
282: 'Reconstruir servicio',
|
||||
283: 'Reconstruye el contenedor del servicio. Solo es necesario si hay un error en StartOS',
|
||||
@@ -287,7 +285,6 @@ export default {
|
||||
286: 'Panel de control',
|
||||
287: 'panel de control',
|
||||
288: 'acciones',
|
||||
289: 'instrucciones',
|
||||
290: 'registros',
|
||||
291: 'acerca de',
|
||||
292: 'Iniciando carga',
|
||||
@@ -485,7 +482,7 @@ export default {
|
||||
484: 'Actualiza la página. Si actualizar no funciona, puede que necesites cerrar y volver a abrir tu navegador, y luego volver a esta página.',
|
||||
485: 'Interfaz de StartOS',
|
||||
486: 'WiFi',
|
||||
487: 'Instrucciones',
|
||||
487: 'Documentación',
|
||||
488: 'español',
|
||||
489: 'polaco',
|
||||
490: 'alemán',
|
||||
@@ -496,7 +493,7 @@ export default {
|
||||
495: 'Validando',
|
||||
496: 'en progreso',
|
||||
497: 'completo',
|
||||
498: 'Haga clic para ver todas las versiones',
|
||||
498: 'Iniciando StartOS',
|
||||
499: 'Para comenzar, visita el Mercado y descarga tu primer servicio',
|
||||
500: 'Ver Marketplace',
|
||||
501: 'Bienvenido a',
|
||||
@@ -522,4 +519,13 @@ export default {
|
||||
521: 'Para resolver el problema, consulta',
|
||||
522: 'Versión de SDK',
|
||||
523: 'Informe de respaldo',
|
||||
524: 'Eliminar seleccionado',
|
||||
525: 'Sin llaves',
|
||||
526: 'Agregar clave pública SSH',
|
||||
527: 'De forma predeterminada, puedes conectarte por SSH a tu servidor desde cualquier dispositivo usando tu contraseña maestra. Opcionalmente, añade claves públicas SSH para otorgar acceso a dispositivos específicos sin necesidad de ingresar una contraseña.',
|
||||
528: 'Código fuente',
|
||||
529: 'Servicio original',
|
||||
530: 'Paquete StartOS',
|
||||
531: 'Error al inicializar el servidor',
|
||||
532: 'Finalizado',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -274,11 +274,9 @@ export default {
|
||||
273: 'Hash Git',
|
||||
274: 'Licence',
|
||||
275: 'Installé depuis',
|
||||
276: 'Dépôt du service',
|
||||
277: 'Dépôt du paquet',
|
||||
278: 'Site marketing',
|
||||
279: 'Site de support',
|
||||
280: 'Lien de don',
|
||||
278: 'Marketing',
|
||||
279: 'Support',
|
||||
280: 'Dons',
|
||||
281: 'Actions standards',
|
||||
282: 'Reconstruire le service',
|
||||
283: 'Reconstruit le conteneur du service. Nécessaire uniquement en cas de bug dans StartOS',
|
||||
@@ -287,7 +285,6 @@ export default {
|
||||
286: 'Tableau de bord',
|
||||
287: 'tableau de bord',
|
||||
288: 'actions',
|
||||
289: 'instructions',
|
||||
290: 'journaux',
|
||||
291: 'à propos',
|
||||
292: 'Début du téléversement',
|
||||
@@ -485,7 +482,7 @@ export default {
|
||||
484: 'Rafraîchissez la page. Si cela ne fonctionne pas, quittez puis rouvrez votre navigateur et revenez sur cette page.',
|
||||
485: 'Interface de StartOS',
|
||||
486: 'WiFi',
|
||||
487: 'Instructions',
|
||||
487: 'Documentation',
|
||||
488: 'espagnol',
|
||||
489: 'polonais',
|
||||
490: 'allemand',
|
||||
@@ -496,7 +493,7 @@ export default {
|
||||
495: 'Validation',
|
||||
496: 'en cours',
|
||||
497: 'terminé',
|
||||
498: 'Cliquez pour voir toutes les versions',
|
||||
498: 'Démarrage de StartOS',
|
||||
499: 'Pour commencer, visitez la bibliothèque de services et téléchargez votre premier service',
|
||||
500: 'Voir la bibliothèque de services',
|
||||
501: 'Bienvenue sur',
|
||||
@@ -522,4 +519,13 @@ export default {
|
||||
521: 'Pour résoudre le problème, consultez',
|
||||
522: 'Version de SDK',
|
||||
523: 'Rapport de sauvegarde',
|
||||
524: 'Supprimer la sélection',
|
||||
525: 'Pas de clés',
|
||||
526: 'Ajouter une clé publique SSH',
|
||||
527: 'Par défaut, vous pouvez accéder à votre serveur en SSH depuis n’importe quel appareil en utilisant votre mot de passe maître. Vous pouvez également ajouter des clés publiques SSH pour accorder l’accès à certains appareils sans avoir à saisir de mot de passe.',
|
||||
528: 'Code source',
|
||||
529: 'Service en amont',
|
||||
530: 'Paquet StartOS',
|
||||
531: "Erreur lors de l'initialisation du serveur",
|
||||
532: 'Terminé',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -274,11 +274,9 @@ export default {
|
||||
273: 'Hash Git',
|
||||
274: 'Licencja',
|
||||
275: 'Zainstalowano z',
|
||||
276: 'Repozytorium serwisu',
|
||||
277: 'Repozytorium pakietu',
|
||||
278: 'Strona marketingowa',
|
||||
279: 'Strona wsparcia',
|
||||
280: 'Link do darowizny',
|
||||
278: 'Marketingowa',
|
||||
279: 'Wsparcia',
|
||||
280: 'Darowizny',
|
||||
281: 'Standardowe akcje',
|
||||
282: 'Odbuduj serwis',
|
||||
283: 'Odbudowuje kontener serwisu. Konieczne tylko w przypadku błędu w StartOS',
|
||||
@@ -287,7 +285,6 @@ export default {
|
||||
286: 'Panel',
|
||||
287: 'panel',
|
||||
288: 'akcje',
|
||||
289: 'instrukcje',
|
||||
290: 'logi',
|
||||
291: 'informacje',
|
||||
292: 'Rozpoczynanie przesyłania',
|
||||
@@ -485,7 +482,7 @@ export default {
|
||||
484: 'Odśwież stronę. Jeśli odświeżenie strony nie działa, może być konieczne zamknięcie i ponowne otwarcie przeglądarki, a następnie ponowne odwiedzenie tej strony.',
|
||||
485: 'Przyłącza StartOS',
|
||||
486: 'WiFi',
|
||||
487: 'Instrukcje',
|
||||
487: 'Dokumentacji',
|
||||
488: 'hiszpański',
|
||||
489: 'polski',
|
||||
490: 'niemiecki',
|
||||
@@ -496,7 +493,7 @@ export default {
|
||||
495: 'Weryfikowanie',
|
||||
496: 'w toku',
|
||||
497: 'zakończono',
|
||||
498: 'Kliknij, aby zobaczyć wszystkie wersje',
|
||||
498: 'Uruchamianie StartOS',
|
||||
499: 'Aby rozpocząć, odwiedź Marketplace i pobierz swoją pierwszą usługę',
|
||||
500: 'Zobacz Rynek',
|
||||
501: 'Witamy w',
|
||||
@@ -522,4 +519,13 @@ export default {
|
||||
521: 'Aby rozwiązać problem, zapoznaj się z',
|
||||
522: 'Wersja SDK',
|
||||
523: 'Raport kopii zapasowej',
|
||||
524: 'Usuń wybrane',
|
||||
525: 'Brak kluczy',
|
||||
526: 'Dodaj klucz publiczny SSH',
|
||||
527: 'Domyślnie możesz połączyć się z serwerem przez SSH z dowolnego urządzenia, używając hasła głównego. Opcjonalnie dodaj klucze publiczne SSH, aby przyznać dostęp określonym urządzeniom bez potrzeby wpisywania hasła.',
|
||||
528: 'Kod źródłowy',
|
||||
529: 'Usługa źródłowa',
|
||||
530: 'Pakiet StartOS',
|
||||
531: 'Błąd inicjalizacji serwera',
|
||||
532: 'Zakończono',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
export * from './classes/http-error'
|
||||
export * from './classes/rpc-error'
|
||||
|
||||
export * from './components/logs-window.component'
|
||||
export * from './components/initializing.component'
|
||||
export * from './components/initializing/logs-window.component'
|
||||
export * from './components/initializing/initializing.component'
|
||||
export * from './components/ticker.component'
|
||||
export * from './components/drive.component'
|
||||
export * from './components/markdown.component'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { Inject, Injectable, DOCUMENT } from '@angular/core'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { Inject, Injectable, DOCUMENT } from '@angular/core'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import {
|
||||
firstValueFrom,
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { StaticClassProvider } from '@angular/core'
|
||||
import {
|
||||
bufferTime,
|
||||
catchError,
|
||||
defer,
|
||||
delay,
|
||||
filter,
|
||||
map,
|
||||
Observable,
|
||||
repeatWhen,
|
||||
scan,
|
||||
switchMap,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import { FollowLogsReq, FollowLogsRes, Log } from '../types/api'
|
||||
import { Constructor } from '../types/constructor'
|
||||
import { convertAnsi } from '../util/convert-ansi'
|
||||
|
||||
interface Api {
|
||||
followServerLogs: (params: FollowLogsReq) => Promise<FollowLogsRes>
|
||||
initFollowLogs: (params: FollowLogsReq) => Promise<FollowLogsRes>
|
||||
openWebsocket$: (guid: string) => Observable<Log>
|
||||
}
|
||||
|
||||
@@ -28,12 +32,14 @@ export function provideSetupLogsService(
|
||||
}
|
||||
|
||||
export class SetupLogsService extends Observable<readonly string[]> {
|
||||
private readonly log$ = defer(() => this.api.followServerLogs({})).pipe(
|
||||
private readonly log$ = defer(() => this.api.initFollowLogs({})).pipe(
|
||||
switchMap(({ guid }) => this.api.openWebsocket$(guid)),
|
||||
bufferTime(500),
|
||||
filter(logs => !!logs.length),
|
||||
map(convertAnsi),
|
||||
scan((logs: readonly string[], log) => [...logs, log], []),
|
||||
repeatWhen(obs => obs.pipe(delay(500))),
|
||||
catchError((_, watch$) => timer(500).pipe(switchMap(() => watch$))),
|
||||
)
|
||||
|
||||
constructor(private readonly api: Api) {
|
||||
|
||||
@@ -98,9 +98,6 @@ $wide-modal: 900px;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
@@ -174,7 +171,7 @@ a {
|
||||
font-weight: 300;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06rem;
|
||||
margin-bottom: 1rem;
|
||||
margin: 0rem 0 1rem 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
(tuiAlertChange)="onDismiss()"
|
||||
>
|
||||
{{ 'New notifications' | i18n }}
|
||||
<a
|
||||
tuiLink
|
||||
routerLink="/portal/notifications"
|
||||
[queryParams]="{ toast: true }"
|
||||
>
|
||||
<a tuiLink routerLink="/notifications" [queryParams]="{ toast: true }">
|
||||
{{ 'View' | i18n }}
|
||||
</a>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
<ng-container *ngIf="!restarted; else refresh">
|
||||
@if (!restarted) {
|
||||
<h1 class="title">StartOS - {{ 'Diagnostic Mode' | i18n }}</h1>
|
||||
|
||||
<ng-container *ngIf="error">
|
||||
@if (error) {
|
||||
<h2 class="subtitle">StartOS {{ 'launch error' | i18n }}:</h2>
|
||||
<code class="code warning">
|
||||
<p>{{ error.problem }}</p>
|
||||
<p *ngIf="error.details">{{ error.details }}</p>
|
||||
@if (error.details) {
|
||||
<p>{{ error.details }}</p>
|
||||
}
|
||||
</code>
|
||||
|
||||
<a tuiButton routerLink="logs">{{ 'View logs' | i18n }}</a>
|
||||
|
||||
<h2 class="subtitle">{{ 'Possible solutions' | i18n }}:</h2>
|
||||
<code class="code"><p>{{ error.solution }}</p></code>
|
||||
|
||||
<code class="code">
|
||||
<p>{{ error.solution }}</p>
|
||||
</code>
|
||||
<div class="buttons">
|
||||
<button tuiButton (click)="restart()">{{ 'Restart server' | i18n }}</button>
|
||||
|
||||
<button
|
||||
*ngIf="error.code === 15 || error.code === 25"
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
(click)="forgetDrive()"
|
||||
>
|
||||
{{ error.code === 15 ? ('Setup current drive' | i18n) : ('Enter recovery mode' | i18n) }}
|
||||
<button tuiButton (click)="restart()">
|
||||
{{ 'Restart server' | i18n }}
|
||||
</button>
|
||||
|
||||
@if (error.code === 15 || error.code === 25) {
|
||||
<button tuiButton appearance="secondary" (click)="forgetDrive()">
|
||||
{{
|
||||
error.code === 15
|
||||
? ('Setup current drive' | i18n)
|
||||
: ('Enter recovery mode' | i18n)
|
||||
}}
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary-destructive"
|
||||
@@ -33,13 +34,11 @@
|
||||
{{ 'Repair drive' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #refresh>
|
||||
}
|
||||
} @else {
|
||||
<h1 class="title">{{ 'Server is restarting' | i18n }}</h1>
|
||||
<h2 class="subtitle">
|
||||
{{ 'Wait for the server to restart, then refresh this page.' | i18n }}
|
||||
</h2>
|
||||
<button tuiButton (click)="refreshPage()">{{ 'Refresh' | i18n }}</button>
|
||||
</ng-template>
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
@Component({
|
||||
selector: 'diagnostic-home',
|
||||
templateUrl: 'home.page.html',
|
||||
templateUrl: 'home.component.html',
|
||||
styleUrls: ['home.page.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
|
||||
@@ -6,7 +6,16 @@ import {
|
||||
provideSetupLogsService,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { catchError, defer, from, map, startWith, switchMap, tap } from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
defer,
|
||||
from,
|
||||
map,
|
||||
startWith,
|
||||
switchMap,
|
||||
tap,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@@ -29,7 +38,7 @@ export default class InitializingPage {
|
||||
.openWebsocket$<T.FullProgress>(guid, {
|
||||
closeObserver: {
|
||||
next: () => {
|
||||
this.state.syncState()
|
||||
this.state.retrigger(true)
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -38,13 +47,12 @@ export default class InitializingPage {
|
||||
map(formatProgress),
|
||||
tap(({ total }) => {
|
||||
if (total === 1) {
|
||||
this.state.syncState()
|
||||
this.state.retrigger(true)
|
||||
}
|
||||
}),
|
||||
catchError((e, caught$) => {
|
||||
console.error(e)
|
||||
this.state.syncState()
|
||||
return caught$
|
||||
catchError((_, caught$) => {
|
||||
this.state.retrigger(true)
|
||||
return timer(500).pipe(switchMap(() => caught$))
|
||||
}),
|
||||
),
|
||||
{ initialValue: { total: 0, message: 'waiting...' } },
|
||||
|
||||
@@ -1,97 +1,92 @@
|
||||
<div
|
||||
*ngIf="!caTrusted; else trusted"
|
||||
tuiCardLarge
|
||||
tuiSurface="floating"
|
||||
class="card"
|
||||
>
|
||||
<tui-icon icon="@tui.lock" [style.font-size.rem]="4" />
|
||||
<h1>{{ 'Trust your Root CA' | i18n }}</h1>
|
||||
<p>
|
||||
{{
|
||||
'Download and trust your Root Certificate Authority to establish a secure (HTTPS) connection. You will need to repeat this on every device you use to connect to your server.'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<b>{{ 'Bookmark this page' | i18n }}</b>
|
||||
-
|
||||
@if (!caTrusted) {
|
||||
<div tuiCardLarge tuiSurface="floating" class="card">
|
||||
<tui-icon icon="@tui.lock" [style.font-size.rem]="4" />
|
||||
<h1>{{ 'Trust your Root CA' | i18n }}</h1>
|
||||
<p>
|
||||
{{
|
||||
'Save this page so you can access it later. You can also find this address in the file downloaded at the end of initial setup.'
|
||||
'Download and trust your Root Certificate Authority to establish a secure (HTTPS) connection. You will need to repeat this on every device you use to connect to your server.'
|
||||
| i18n
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
<b>{{ 'Download your Root CA' | i18n }}</b>
|
||||
-
|
||||
{{
|
||||
'Your server uses its Root CA to generate SSL/TLS certificates for itself and installed services. These certificates are then used to encrypt network traffic with your client devices.'
|
||||
| i18n
|
||||
}}
|
||||
<br />
|
||||
<a
|
||||
tuiButton
|
||||
size="s"
|
||||
iconEnd="@tui.download"
|
||||
href="/static/local-root-ca.crt"
|
||||
>
|
||||
{{ 'Download' | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<b>{{ 'Trust your Root CA' | i18n }}</b>
|
||||
-
|
||||
{{
|
||||
'Follow instructions for your OS. By trusting your Root CA, your device can verify the authenticity of encrypted communications with your server.'
|
||||
| i18n
|
||||
}}
|
||||
<br />
|
||||
<a
|
||||
tuiButton
|
||||
docsLink
|
||||
size="s"
|
||||
href="/user-manual/trust-ca.html"
|
||||
iconEnd="@tui.external-link"
|
||||
>
|
||||
{{ 'View instructions' | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<b>{{ 'Test' | i18n }}</b>
|
||||
-
|
||||
{{
|
||||
'Refresh the page. If refreshing the page does not work, you may need to quit and re-open your browser, then revisit this page.'
|
||||
| i18n
|
||||
}}
|
||||
<br />
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
class="refresh"
|
||||
appearance="positive"
|
||||
iconEnd="@tui.refresh-cw"
|
||||
(click)="refresh()"
|
||||
>
|
||||
{{ 'Refresh' | i18n }}
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
appearance="flat-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
(click)="launchHttps()"
|
||||
[disabled]="caTrusted"
|
||||
>
|
||||
{{ 'Skip' | i18n }}
|
||||
</button>
|
||||
<div>
|
||||
<small>({{ 'not recommended' | i18n }})</small>
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<b>{{ 'Bookmark this page' | i18n }}</b>
|
||||
-
|
||||
{{
|
||||
'Save this page so you can access it later. You can also find this address in the file downloaded at the end of initial setup.'
|
||||
| i18n
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
<b>{{ 'Download your Root CA' | i18n }}</b>
|
||||
-
|
||||
{{
|
||||
'Your server uses its Root CA to generate SSL/TLS certificates for itself and installed services. These certificates are then used to encrypt network traffic with your client devices.'
|
||||
| i18n
|
||||
}}
|
||||
<br />
|
||||
<a
|
||||
tuiButton
|
||||
size="s"
|
||||
iconEnd="@tui.download"
|
||||
href="/static/local-root-ca.crt"
|
||||
>
|
||||
{{ 'Download' | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<b>{{ 'Trust your Root CA' | i18n }}</b>
|
||||
-
|
||||
{{
|
||||
'Follow instructions for your OS. By trusting your Root CA, your device can verify the authenticity of encrypted communications with your server.'
|
||||
| i18n
|
||||
}}
|
||||
<br />
|
||||
<a
|
||||
tuiButton
|
||||
docsLink
|
||||
size="s"
|
||||
href="/user-manual/trust-ca.html"
|
||||
iconEnd="@tui.external-link"
|
||||
>
|
||||
{{ 'View instructions' | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<b>{{ 'Test' | i18n }}</b>
|
||||
-
|
||||
{{
|
||||
'Refresh the page. If refreshing the page does not work, you may need to quit and re-open your browser, then revisit this page.'
|
||||
| i18n
|
||||
}}
|
||||
<br />
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
class="refresh"
|
||||
appearance="positive"
|
||||
iconEnd="@tui.refresh-cw"
|
||||
(click)="refresh()"
|
||||
>
|
||||
{{ 'Refresh' | i18n }}
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
<button
|
||||
tuiButton
|
||||
size="s"
|
||||
appearance="flat-grayscale"
|
||||
iconEnd="@tui.external-link"
|
||||
(click)="launchHttps()"
|
||||
[disabled]="caTrusted"
|
||||
>
|
||||
{{ 'Skip' | i18n }}
|
||||
</button>
|
||||
<div>
|
||||
<small>({{ 'not recommended' | i18n }})</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #trusted>
|
||||
} @else {
|
||||
<div tuiCardLarge tuiSurface="floating" class="card">
|
||||
<tui-icon icon="@tui.shield" class="g-positive" [style.font-size.rem]="4" />
|
||||
<h1>{{ 'Root CA Trusted!' | i18n }}</h1>
|
||||
@@ -105,4 +100,4 @@
|
||||
{{ 'Go to login' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CommonModule, DOCUMENT } from '@angular/common'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { Component, inject, DOCUMENT } from '@angular/core'
|
||||
import { DocsLinkDirective, i18nPipe, RELATIVE_URL } from '@start9labs/shared'
|
||||
import { TuiButton, TuiIcon, TuiSurface } from '@taiga-ui/core'
|
||||
import { TuiCardLarge } from '@taiga-ui/layout'
|
||||
@@ -11,7 +10,6 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
templateUrl: './ca-wizard.component.html',
|
||||
styleUrls: ['./ca-wizard.component.scss'],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TuiIcon,
|
||||
TuiButton,
|
||||
TuiCardLarge,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- Local HTTP -->
|
||||
<ca-wizard *ngIf="config.isLanHttp(); else notLanHttp"></ca-wizard>
|
||||
|
||||
<!-- not Local HTTP -->
|
||||
<ng-template #notLanHttp>
|
||||
@if (config.isLanHttp()) {
|
||||
<!-- Local HTTP -->
|
||||
<ca-wizard />
|
||||
} @else {
|
||||
<!-- not Local HTTP -->
|
||||
<div tuiCardLarge class="card">
|
||||
<img alt="StartOS Icon" class="logo" src="assets/img/icon.png" />
|
||||
<h1 class="header">{{'Login to StartOS' | i18n}}</h1>
|
||||
<h1 class="header">{{ 'Login to StartOS' | i18n }}</h1>
|
||||
<form (submit)="submit()">
|
||||
<tui-input-password
|
||||
tuiTextfieldIconLeft="@tui.key"
|
||||
@@ -13,10 +13,10 @@
|
||||
[(ngModel)]="password"
|
||||
(ngModelChange)="error = null"
|
||||
>
|
||||
{{'Password' | i18n}}
|
||||
{{ 'Password' | i18n }}
|
||||
</tui-input-password>
|
||||
<tui-error class="error" [error]="error || null" />
|
||||
<button tuiButton class="button">{{'Login' | i18n}}</button>
|
||||
<button tuiButton class="button">{{ 'Login' | i18n }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Router } from '@angular/router'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import { Component, Inject, DestroyRef, inject } from '@angular/core'
|
||||
import { Component, Inject, DestroyRef, inject, DOCUMENT } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { AuthService } from 'src/app/services/auth.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { i18nKey, LoadingService } from '@start9labs/shared'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'login',
|
||||
templateUrl: './login.page.html',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.page.scss'],
|
||||
providers: [],
|
||||
standalone: false,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -46,17 +45,17 @@ export interface FormContext<T> {
|
||||
<form-group [spec]="spec" />
|
||||
<footer>
|
||||
<ng-content />
|
||||
<ng-container *ngFor="let button of buttons; let last = last">
|
||||
<button
|
||||
*ngIf="button.handler; else link"
|
||||
tuiButton
|
||||
[appearance]="last ? 'primary' : 'flat-grayscale'"
|
||||
[type]="last ? 'submit' : 'button'"
|
||||
(click)="onClick(button.handler)"
|
||||
>
|
||||
{{ button.text }}
|
||||
</button>
|
||||
<ng-template #link>
|
||||
@for (button of buttons; track $index) {
|
||||
@if (button.handler) {
|
||||
<button
|
||||
tuiButton
|
||||
[appearance]="$last ? 'primary' : 'flat-grayscale'"
|
||||
[type]="$last ? 'submit' : 'button'"
|
||||
(click)="onClick(button.handler)"
|
||||
>
|
||||
{{ button.text }}
|
||||
</button>
|
||||
} @else {
|
||||
<a
|
||||
tuiButton
|
||||
appearance="flat-grayscale"
|
||||
@@ -65,8 +64,8 @@ export interface FormContext<T> {
|
||||
>
|
||||
{{ button.text }}
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</footer>
|
||||
</form>
|
||||
`,
|
||||
@@ -85,7 +84,6 @@ export interface FormContext<T> {
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule,
|
||||
TuiValueChanges,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<div class="label">
|
||||
{{ spec.name }}
|
||||
<tui-icon
|
||||
*ngIf="spec.description || spec.disabled"
|
||||
[tuiTooltip]="spec | hint"
|
||||
/>
|
||||
@if (spec.description || spec.disabled) {
|
||||
<tui-icon [tuiTooltip]="spec | hint" />
|
||||
}
|
||||
<button
|
||||
tuiLink
|
||||
type="button"
|
||||
@@ -14,24 +13,24 @@
|
||||
+ {{ 'Add' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<tui-error [error]="order | tuiFieldError | async"></tui-error>
|
||||
<tui-error [error]="order | tuiFieldError | async" />
|
||||
|
||||
<ng-container *ngFor="let item of array.control.controls; let index = index">
|
||||
<form-object
|
||||
*ngIf="spec.spec.type === 'object'; else control"
|
||||
class="object"
|
||||
[class.object_open]="!!open.get(item)"
|
||||
[formGroup]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
[open]="!!open.get(item)"
|
||||
(openChange)="open.set(item, $event)"
|
||||
>
|
||||
{{ item.value | mustache: $any(spec.spec).displayAs }}
|
||||
<ng-container *ngTemplateOutlet="remove"></ng-container>
|
||||
</form-object>
|
||||
<ng-template #control>
|
||||
@for (item of array.control.controls; track item) {
|
||||
@if (spec.spec.type === 'object') {
|
||||
<form-object
|
||||
class="object"
|
||||
[class.object_open]="!!open.get(item)"
|
||||
[formGroup]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
[open]="!!open.get(item)"
|
||||
(openChange)="open.set(item, $event)"
|
||||
>
|
||||
{{ item.value | mustache: $any(spec.spec).displayAs }}
|
||||
<ng-container *ngTemplateOutlet="remove" />
|
||||
</form-object>
|
||||
} @else {
|
||||
<form-control
|
||||
class="control"
|
||||
tuiTextfieldSize="m"
|
||||
@@ -41,8 +40,8 @@
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
></form-control>
|
||||
</ng-template>
|
||||
/>
|
||||
}
|
||||
<ng-template #remove>
|
||||
<button
|
||||
tuiIconButton
|
||||
@@ -52,7 +51,7 @@
|
||||
appearance="icon"
|
||||
size="m"
|
||||
title="Remove"
|
||||
(click.stop)="removeAt(index)"
|
||||
(click.stop)="removeAt($index)"
|
||||
></button>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@@ -10,18 +10,21 @@
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</tui-input>
|
||||
<ng-template #color>
|
||||
<div class="wrapper" [style.color]="value">
|
||||
<input
|
||||
*ngIf="!readOnly && !spec.disabled"
|
||||
type="color"
|
||||
class="color"
|
||||
tabindex="-1"
|
||||
[(ngModel)]="value"
|
||||
(click.stop)="(0)"
|
||||
/>
|
||||
@if (!readOnly && !spec.disabled) {
|
||||
<input
|
||||
type="color"
|
||||
class="color"
|
||||
tabindex="-1"
|
||||
[(ngModel)]="value"
|
||||
(click.stop)="(0)"
|
||||
/>
|
||||
}
|
||||
<tui-icon icon="@tui.paint-bucket" tuiAppearance="icon" class="icon" />
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,40 +1,58 @@
|
||||
<ng-container [ngSwitch]="spec.type">
|
||||
<form-color *ngSwitchCase="'color'"></form-color>
|
||||
<form-datetime *ngSwitchCase="'datetime'"></form-datetime>
|
||||
<form-file *ngSwitchCase="'file'"></form-file>
|
||||
<form-multiselect *ngSwitchCase="'multiselect'"></form-multiselect>
|
||||
<form-number *ngSwitchCase="'number'"></form-number>
|
||||
<form-select *ngSwitchCase="'select'"></form-select>
|
||||
<form-text *ngSwitchCase="'text'"></form-text>
|
||||
<form-textarea *ngSwitchCase="'textarea'"></form-textarea>
|
||||
<form-toggle *ngSwitchCase="'toggle'"></form-toggle>
|
||||
</ng-container>
|
||||
<tui-error [error]="order | tuiFieldError | async"></tui-error>
|
||||
<ng-template
|
||||
*ngIf="spec.warning || immutable"
|
||||
#warning
|
||||
let-completeWith="completeWith"
|
||||
>
|
||||
{{ spec.warning }}
|
||||
<p *ngIf="immutable">{{ 'This value cannot be changed once set' | i18n }}!</p>
|
||||
<div class="buttons">
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="secondary"
|
||||
size="s"
|
||||
(click)="completeWith(true)"
|
||||
>
|
||||
{{ 'Cancel' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="flat-grayscale"
|
||||
size="s"
|
||||
(click)="completeWith(false)"
|
||||
>
|
||||
{{'Continue' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
@switch (spec.type) {
|
||||
@case ('color') {
|
||||
<form-color />
|
||||
}
|
||||
@case ('datetime') {
|
||||
<form-datetime />
|
||||
}
|
||||
@case ('file') {
|
||||
<form-file />
|
||||
}
|
||||
@case ('multiselect') {
|
||||
<form-multiselect />
|
||||
}
|
||||
@case ('number') {
|
||||
<form-number />
|
||||
}
|
||||
@case ('select') {
|
||||
<form-select />
|
||||
}
|
||||
@case ('text') {
|
||||
<form-text />
|
||||
}
|
||||
@case ('textarea') {
|
||||
<form-textarea />
|
||||
}
|
||||
@case ('toggle') {
|
||||
<form-toggle />
|
||||
}
|
||||
}
|
||||
<tui-error [error]="order | tuiFieldError | async" />
|
||||
@if (spec.warning || immutable) {
|
||||
<ng-template #warning let-completeWith="completeWith">
|
||||
{{ spec.warning }}
|
||||
@if (immutable) {
|
||||
<p>{{ 'This value cannot be changed once set' | i18n }}!</p>
|
||||
}
|
||||
<div class="buttons">
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="secondary"
|
||||
size="s"
|
||||
(click)="completeWith(true)"
|
||||
>
|
||||
{{ 'Cancel' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="flat-grayscale"
|
||||
size="s"
|
||||
(click)="completeWith(false)"
|
||||
>
|
||||
{{ 'Continue' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
@@ -1,43 +1,54 @@
|
||||
<ng-container [ngSwitch]="spec.inputmode" [tuiHintContent]="spec.description">
|
||||
<tui-input-time
|
||||
*ngSwitchCase="'time'"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[ngModel]="getTime(value)"
|
||||
(ngModelChange)="value = $event?.toString() || null"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
</tui-input-time>
|
||||
<tui-input-date
|
||||
*ngSwitchCase="'date'"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min"
|
||||
[max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
</tui-input-date>
|
||||
<tui-input-date-time
|
||||
*ngSwitchCase="'datetime-local'"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[min]="spec.min ? (spec.min | tuiMapper: getLimit) : min"
|
||||
[max]="spec.max ? (spec.max | tuiMapper: getLimit) : max"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
</tui-input-date-time>
|
||||
<ng-container [tuiHintContent]="spec.description">
|
||||
@switch (spec.inputmode) {
|
||||
@case ('time') {
|
||||
<tui-input-time
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[ngModel]="getTime(value)"
|
||||
(ngModelChange)="value = $event?.toString() || null"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</tui-input-time>
|
||||
}
|
||||
@case ('date') {
|
||||
<tui-input-date
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min"
|
||||
[max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</tui-input-date>
|
||||
}
|
||||
@case ('datetime-local') {
|
||||
<tui-input-date-time
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[min]="spec.min ? (spec.min | tuiMapper: getLimit) : min"
|
||||
[max]="spec.max ? (spec.max | tuiMapper: getLimit) : max"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
</tui-input-date-time>
|
||||
}
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
<ng-container
|
||||
*ngFor="let entry of spec | keyvalue: asIsOrder | filterHidden"
|
||||
[ngSwitch]="entry.value.type"
|
||||
[tuiTextfieldCleaner]="true"
|
||||
>
|
||||
<form-object
|
||||
*ngSwitchCase="'object'"
|
||||
class="g-form-control"
|
||||
[formGroupName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
></form-object>
|
||||
<form-union
|
||||
*ngSwitchCase="'union'"
|
||||
class="g-form-control"
|
||||
[formGroupName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
></form-union>
|
||||
<form-array
|
||||
*ngSwitchCase="'list'"
|
||||
[formArrayName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
></form-array>
|
||||
<form-control
|
||||
*ngSwitchDefault
|
||||
class="g-form-control"
|
||||
[formControlName]="entry.key"
|
||||
[spec]="entry.value"
|
||||
></form-control>
|
||||
</ng-container>
|
||||
@for (entry of spec | keyvalue: asIsOrder | filterHidden; track entry) {
|
||||
<ng-container [tuiTextfieldCleaner]="true">
|
||||
@switch (entry.value.type) {
|
||||
@case ('object') {
|
||||
<form-object
|
||||
class="g-form-control"
|
||||
[formGroupName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
/>
|
||||
}
|
||||
@case ('union') {
|
||||
<form-union
|
||||
class="g-form-control"
|
||||
[formGroupName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
/>
|
||||
}
|
||||
@case ('list') {
|
||||
<form-array [formArrayName]="entry.key" [spec]="$any(entry.value)" />
|
||||
}
|
||||
@default {
|
||||
<form-control
|
||||
class="g-form-control"
|
||||
[formControlName]="entry.key"
|
||||
[spec]="entry.value"
|
||||
/>
|
||||
}
|
||||
}
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[pseudoInvalid]="invalid"
|
||||
[tuiNumberFormat]="{
|
||||
precision: spec.integer ? 0 : Infinity,
|
||||
decimalMode: 'not-zero'
|
||||
decimalMode: 'not-zero',
|
||||
}"
|
||||
[min]="spec.min ?? -Infinity"
|
||||
[max]="spec.max ?? Infinity"
|
||||
@@ -15,6 +15,8 @@
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
<input tuiTextfieldLegacy [placeholder]="spec.placeholder || ''" />
|
||||
</tui-input-number>
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
<input
|
||||
tuiTextfieldLegacy
|
||||
[class.masked]="spec.masked && masked"
|
||||
@@ -19,26 +21,28 @@
|
||||
/>
|
||||
</tui-input>
|
||||
<ng-template #toggle>
|
||||
<button
|
||||
*ngIf="spec.generate"
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Generate"
|
||||
size="xs"
|
||||
class="button"
|
||||
iconStart="@tui.refresh-ccw"
|
||||
(click)="generate()"
|
||||
></button>
|
||||
<button
|
||||
*ngIf="spec.masked"
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Toggle masking"
|
||||
size="xs"
|
||||
class="button"
|
||||
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
@if (spec.generate) {
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Generate"
|
||||
size="xs"
|
||||
class="button"
|
||||
iconStart="@tui.refresh-ccw"
|
||||
(click)="generate()"
|
||||
></button>
|
||||
}
|
||||
@if (spec.masked) {
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Toggle masking"
|
||||
size="xs"
|
||||
class="button"
|
||||
[iconStart]="masked ? '@tui.eye' : '@tui.eye-off'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
@if (spec.required) {
|
||||
<span>*</span>
|
||||
}
|
||||
<textarea
|
||||
tuiTextfieldLegacy
|
||||
[placeholder]="spec.placeholder || ''"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { CopyService, i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiFade } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
@@ -46,6 +47,20 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
{{ 'Copy' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div tuiCell>
|
||||
<div tuiTitle>
|
||||
<strong>Public Key</strong>
|
||||
<div tuiSubtitle tuiFade>{{ getPubkey(server) }}</div>
|
||||
</div>
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="icon"
|
||||
iconStart="@tui.copy"
|
||||
(click)="copyService.copy(getPubkey(server))"
|
||||
>
|
||||
{{ 'Copy' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: '[tuiCell] { padding-inline: 0; white-space: nowrap }',
|
||||
@@ -58,6 +73,10 @@ export class AboutComponent {
|
||||
readonly server = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo'),
|
||||
)
|
||||
|
||||
getPubkey(server: T.ServerInfo) {
|
||||
return `${server.pubkey} startos@${server.hostname}`
|
||||
}
|
||||
}
|
||||
|
||||
export const ABOUT = new PolymorpheusComponent(AboutComponent)
|
||||
|
||||
@@ -72,7 +72,7 @@ import { ABOUT } from './about.component'
|
||||
<a
|
||||
tuiOption
|
||||
iconStart="@tui.settings"
|
||||
routerLink="/portal/system"
|
||||
routerLink="/system"
|
||||
(click)="open = false"
|
||||
>
|
||||
{{ 'System Settings' | i18n }}
|
||||
|
||||
@@ -22,9 +22,9 @@ import { getMenu } from 'src/app/utils/system-utilities'
|
||||
class="link"
|
||||
routerLinkActive="link_active"
|
||||
tuiHintDirection="bottom"
|
||||
[tuiHintShowDelay]="750"
|
||||
[routerLink]="item.routerLink"
|
||||
[class.link_system]="item.routerLink === '/portal/system'"
|
||||
[tuiHintShowDelay]="250"
|
||||
[routerLink]="['/', item.routerLink]"
|
||||
[class.link_system]="item.routerLink === 'system'"
|
||||
[tuiHint]="rla.isActive ? '' : (item.name | i18n)"
|
||||
>
|
||||
<tui-badged-content
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
DOCUMENT,
|
||||
} from '@angular/core'
|
||||
import { CopyService, DialogService, i18nPipe } from '@start9labs/shared'
|
||||
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { QRModal } from 'src/app/routes/portal/modals/qr.component'
|
||||
import { InterfaceComponent } from './interface.component'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'td[actions]',
|
||||
|
||||
@@ -74,7 +74,7 @@ export class LogsPipe implements PipeTransform {
|
||||
this.logs.status$.next(v ? 'reconnecting' : 'disconnected'),
|
||||
),
|
||||
filter(Boolean),
|
||||
delay(1000),
|
||||
delay(1000), // @TODO Alex why delay here?
|
||||
take(1),
|
||||
ignoreElements(),
|
||||
),
|
||||
|
||||
@@ -14,7 +14,7 @@ import { TuiBadgeNotification } from '@taiga-ui/kit'
|
||||
import { BadgeService } from 'src/app/services/badge.service'
|
||||
import { getMenu } from 'src/app/utils/system-utilities'
|
||||
|
||||
const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
|
||||
const FILTER = ['/services', '/system', '/marketplace']
|
||||
|
||||
@Component({
|
||||
selector: 'app-tabs',
|
||||
@@ -23,7 +23,7 @@ const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
|
||||
<a
|
||||
tuiTabBarItem
|
||||
icon="@tui.layout-grid"
|
||||
routerLink="/portal/services"
|
||||
routerLink="/services"
|
||||
routerLinkActive
|
||||
(isActiveChange)="update()"
|
||||
>
|
||||
@@ -32,7 +32,7 @@ const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
|
||||
<a
|
||||
tuiTabBarItem
|
||||
icon="@tui.shopping-cart"
|
||||
routerLink="/portal/marketplace"
|
||||
routerLink="/marketplace"
|
||||
routerLinkActive
|
||||
(isActiveChange)="update()"
|
||||
>
|
||||
@@ -41,7 +41,7 @@ const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
|
||||
<a
|
||||
tuiTabBarItem
|
||||
icon="@tui.settings"
|
||||
routerLink="/portal/system"
|
||||
routerLink="/system"
|
||||
routerLinkActive
|
||||
[badge]="badge()"
|
||||
(isActiveChange)="update()"
|
||||
@@ -60,7 +60,7 @@ const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
|
||||
<a
|
||||
class="item"
|
||||
routerLinkActive="item_active"
|
||||
[routerLink]="item.routerLink"
|
||||
[routerLink]="['/', item.routerLink]"
|
||||
(click)="observer.complete()"
|
||||
>
|
||||
<tui-icon [icon]="item.icon" />
|
||||
@@ -124,7 +124,7 @@ export class TabsComponent {
|
||||
index = 3
|
||||
|
||||
readonly menu = getMenu().filter(item => !FILTER.includes(item.routerLink))
|
||||
readonly badge = toSignal(inject(BadgeService).getCount('/portal/system'), {
|
||||
readonly badge = toSignal(inject(BadgeService).getCount('system'), {
|
||||
initialValue: 0,
|
||||
})
|
||||
|
||||
|
||||
@@ -9,19 +9,22 @@ import { TuiNotification } from '@taiga-ui/core'
|
||||
import { getValueByPointer, Operation } from 'fast-json-patch'
|
||||
import { i18nPipe, isObject } from '@start9labs/shared'
|
||||
import { tuiIsNumber } from '@taiga-ui/cdk'
|
||||
import { CommonModule } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'task-info',
|
||||
template: `
|
||||
<tui-notification *ngIf="diff.length">
|
||||
{{ 'The following modifications were made' | i18n }}:
|
||||
<ul>
|
||||
<li *ngFor="let d of diff" [innerHTML]="d"></li>
|
||||
</ul>
|
||||
</tui-notification>
|
||||
@if (diff.length) {
|
||||
<tui-notification>
|
||||
{{ 'The following modifications were made' | i18n }}:
|
||||
<ul>
|
||||
@for (d of diff; track d) {
|
||||
<li [innerHTML]="d"></li>
|
||||
}
|
||||
</ul>
|
||||
</tui-notification>
|
||||
}
|
||||
`,
|
||||
imports: [CommonModule, TuiNotification, i18nPipe],
|
||||
imports: [TuiNotification, i18nPipe],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styles: `
|
||||
tui-notification {
|
||||
|
||||
@@ -25,50 +25,50 @@ const ROUTES: Routes = [
|
||||
// title: titleResolver,
|
||||
// path: 'backups',
|
||||
// loadComponent: () => import('./routes/backups/backups.component'),
|
||||
// data: toNavigationItem('/portal/backups'),
|
||||
// data: toNavigationItem('backups'),
|
||||
// },
|
||||
{
|
||||
title: titleResolver,
|
||||
path: 'logs',
|
||||
loadChildren: () => import('./routes/logs/logs.routes'),
|
||||
data: toNavigationItem('/portal/logs'),
|
||||
data: toNavigationItem('logs'),
|
||||
},
|
||||
{
|
||||
title: titleResolver,
|
||||
path: 'marketplace',
|
||||
loadChildren: () => import('./routes/marketplace/marketplace.routes'),
|
||||
data: toNavigationItem('/portal/marketplace'),
|
||||
data: toNavigationItem('marketplace'),
|
||||
},
|
||||
{
|
||||
title: titleResolver,
|
||||
path: 'system',
|
||||
loadChildren: () => import('./routes/system/system.routes'),
|
||||
data: toNavigationItem('/portal/system'),
|
||||
data: toNavigationItem('system'),
|
||||
},
|
||||
{
|
||||
title: titleResolver,
|
||||
path: 'notifications',
|
||||
loadComponent: () =>
|
||||
import('./routes/notifications/notifications.component'),
|
||||
data: toNavigationItem('/portal/notifications'),
|
||||
data: toNavigationItem('notifications'),
|
||||
},
|
||||
{
|
||||
title: titleResolver,
|
||||
path: 'sideload',
|
||||
loadComponent: () => import('./routes/sideload/sideload.component'),
|
||||
data: toNavigationItem('/portal/sideload'),
|
||||
data: toNavigationItem('sideload'),
|
||||
},
|
||||
{
|
||||
title: titleResolver,
|
||||
path: 'updates',
|
||||
loadComponent: () => import('./routes/updates/updates.component'),
|
||||
data: toNavigationItem('/portal/updates'),
|
||||
data: toNavigationItem('updates'),
|
||||
},
|
||||
{
|
||||
title: titleResolver,
|
||||
path: 'metrics',
|
||||
loadComponent: () => import('./routes/metrics/metrics.component'),
|
||||
data: toNavigationItem('/portal/metrics'),
|
||||
data: toNavigationItem('metrics'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject } from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
@@ -58,19 +57,21 @@ import { TARGET, TARGET_CREATE } from './target.component'
|
||||
Schedule
|
||||
<input tuiTextfieldLegacy placeholder="* * * * *" />
|
||||
</tui-input>
|
||||
<div *ngIf="job.cron | toHumanCron as human" [style.color]="human.color">
|
||||
{{ human.message }}
|
||||
</div>
|
||||
<div *ngIf="!job.job.id" class="g-toggle">
|
||||
Also Execute Now
|
||||
<input
|
||||
tuiSwitch
|
||||
type="checkbox"
|
||||
name="now"
|
||||
[showIcons]="false"
|
||||
[(ngModel)]="job.now"
|
||||
/>
|
||||
</div>
|
||||
@if (job.cron | toHumanCron; as human) {
|
||||
<div [style.color]="human.color">{{ human.message }}</div>
|
||||
}
|
||||
@if (!job.job.id) {
|
||||
<div class="g-toggle">
|
||||
Also Execute Now
|
||||
<input
|
||||
tuiSwitch
|
||||
type="checkbox"
|
||||
name="now"
|
||||
[showIcons]="false"
|
||||
[(ngModel)]="job.now"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
tuiButton
|
||||
class="submit"
|
||||
@@ -96,7 +97,6 @@ import { TARGET, TARGET_CREATE } from './target.component'
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
TuiInputModule,
|
||||
TuiInputNumberModule,
|
||||
|
||||
@@ -17,7 +17,7 @@ import { RecoverOption } from '../types/recover-option'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *ngIf="packageData$ | toOptions: backups | async as options">
|
||||
@if (packageData$ | toOptions: backups | async; as options) {
|
||||
<div
|
||||
tuiGroup
|
||||
orientation="vertical"
|
||||
@@ -30,12 +30,9 @@ import { RecoverOption } from '../types/recover-option'
|
||||
<strong>{{ option.title }}</strong>
|
||||
<div>Version {{ option.version }}</div>
|
||||
<div>Backup made: {{ option.timestamp | date: 'medium' }}</div>
|
||||
<div
|
||||
*ngIf="option | tuiMapper: toMessage as message"
|
||||
[style.color]="message.color"
|
||||
>
|
||||
{{ message.text }}
|
||||
</div>
|
||||
@if (option | tuiMapper: toMessage; as message) {
|
||||
<div [style.color]="message.color">{{ message.text }}</div>
|
||||
}
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -46,7 +43,6 @@ import { RecoverOption } from '../types/recover-option'
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<footer class="g-buttons">
|
||||
<button
|
||||
tuiButton
|
||||
@@ -56,7 +52,7 @@ import { RecoverOption } from '../types/recover-option'
|
||||
Restore Selected
|
||||
</button>
|
||||
</footer>
|
||||
</ng-container>
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, OnInit, signal } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiButton, TuiLink, TuiNotification } from '@taiga-ui/core'
|
||||
@@ -64,7 +63,6 @@ import { DocsLinkDirective } from 'projects/shared/src/public-api'
|
||||
></table>
|
||||
`,
|
||||
imports: [
|
||||
CommonModule,
|
||||
TuiNotification,
|
||||
TuiButton,
|
||||
BackupsPhysicalComponent,
|
||||
|
||||
@@ -68,7 +68,7 @@ export class BackupsRestoreService {
|
||||
),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['/portal/services'])
|
||||
this.router.navigate(['services'])
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
import { KeyValuePipe } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiAppearance, TuiButton, TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiCardMedium } from '@taiga-ui/layout'
|
||||
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
|
||||
interface Log {
|
||||
title: i18nKey
|
||||
subtitle: i18nKey
|
||||
icon: string
|
||||
follow: (params: RR.FollowServerLogsReq) => Promise<RR.FollowServerLogsRes>
|
||||
fetch: (params: RR.GetServerLogsReq) => Promise<RR.GetServerLogsRes>
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *title>
|
||||
@if (current(); as key) {
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.arrow-left"
|
||||
(click)="current.set(null)"
|
||||
>
|
||||
{{ 'Back' | i18n }}
|
||||
</button>
|
||||
{{ logs[key]?.title | i18n }}
|
||||
} @else {
|
||||
{{ 'Logs' | i18n }}
|
||||
}
|
||||
</ng-container>
|
||||
@if (current(); as key) {
|
||||
<header tuiTitle>
|
||||
<strong class="title">
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="secondary-grayscale"
|
||||
iconStart="@tui.x"
|
||||
size="s"
|
||||
class="close"
|
||||
(click)="current.set(null)"
|
||||
>
|
||||
{{ 'Close' | i18n }}
|
||||
</button>
|
||||
{{ logs[key]?.title | i18n }}
|
||||
</strong>
|
||||
<p tuiSubtitle>{{ logs[key]?.subtitle | i18n }}</p>
|
||||
</header>
|
||||
@for (log of logs | keyvalue; track $index) {
|
||||
@if (log.key === current()) {
|
||||
<logs
|
||||
[context]="log.key"
|
||||
[followLogs]="log.value.follow"
|
||||
[fetchLogs]="log.value.fetch"
|
||||
/>
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
@for (log of logs | keyvalue; track $index) {
|
||||
<button
|
||||
tuiCardMedium
|
||||
tuiAppearance="neutral"
|
||||
(click)="current.set(log.key)"
|
||||
>
|
||||
<tui-icon [icon]="log.value.icon" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ log.value.title | i18n }}</strong>
|
||||
<span tuiSubtitle>{{ log.value.subtitle | i18n }}</span>
|
||||
</span>
|
||||
<tui-icon icon="@tui.chevron-right" />
|
||||
</button>
|
||||
}
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'g-page' },
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
logs {
|
||||
height: calc(100% - 4rem);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
button::before {
|
||||
margin: 0 -0.25rem 0 -0.375rem;
|
||||
--tui-icon-size: 1.5rem;
|
||||
}
|
||||
|
||||
[tuiCardMedium] {
|
||||
height: 14rem;
|
||||
width: 14rem;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--tui-background-neutral-1),
|
||||
var(--tui-shadow-small);
|
||||
|
||||
[tuiSubtitle] {
|
||||
color: var(--tui-text-secondary);
|
||||
}
|
||||
|
||||
tui-icon:last-child {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
logs {
|
||||
height: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
[tuiCardMedium] {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
LogsComponent,
|
||||
TitleDirective,
|
||||
KeyValuePipe,
|
||||
TuiTitle,
|
||||
TuiCardMedium,
|
||||
TuiIcon,
|
||||
TuiAppearance,
|
||||
TuiButton,
|
||||
i18nPipe,
|
||||
],
|
||||
})
|
||||
export default class SystemLogsComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
readonly current = signal<string | null>(null)
|
||||
readonly logs: Record<string, Log> = {
|
||||
os: {
|
||||
title: 'OS Logs',
|
||||
subtitle: 'Raw, unfiltered operating system logs',
|
||||
icon: '@tui.square-dashed-bottom-code',
|
||||
follow: params => this.api.followServerLogs(params),
|
||||
fetch: params => this.api.getServerLogs(params),
|
||||
},
|
||||
kernel: {
|
||||
title: 'Kernel Logs',
|
||||
subtitle: 'Diagnostics for drivers and other kernel processes',
|
||||
icon: '@tui.square-chevron-right',
|
||||
follow: params => this.api.followKernelLogs(params),
|
||||
fetch: params => this.api.getKernelLogs(params),
|
||||
},
|
||||
tor: {
|
||||
title: 'Tor Logs',
|
||||
subtitle: 'Diagnostic logs for the Tor daemon on StartOS',
|
||||
icon: '@tui.globe',
|
||||
follow: params => this.api.followTorLogs(params),
|
||||
fetch: params => this.api.getTorLogs(params),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,13 @@ import { CommonModule, TitleCasePipe } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { toObservable, toSignal } from '@angular/core/rxjs-interop'
|
||||
import { Router } from '@angular/router'
|
||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||
import {
|
||||
ErrorService,
|
||||
Exver,
|
||||
@@ -17,29 +20,29 @@ import {
|
||||
} from '@start9labs/shared'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { firstValueFrom } from 'rxjs'
|
||||
import { firstValueFrom, switchMap } from 'rxjs'
|
||||
import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { dryUpdate } from 'src/app/utils/dry-update'
|
||||
import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
|
||||
import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
||||
import { MarketplacePreviewComponent } from '../modals/preview.component'
|
||||
|
||||
import { MarketplaceAlertsService } from '../services/alerts.service'
|
||||
|
||||
type KEYS = 'id' | 'version' | 'alerts' | 'flavor'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-controls',
|
||||
template: `
|
||||
@if (localPkg(); as local) {
|
||||
@if (sameFlavor() && localPkg(); as local) {
|
||||
@if (local.stateInfo.state === 'installed') {
|
||||
@switch ((local | toManifest).version | compareExver: version()) {
|
||||
@switch ((local | toManifest).version | compareExver: pkg().version) {
|
||||
@case (1) {
|
||||
<button
|
||||
tuiButton
|
||||
size="m"
|
||||
type="button"
|
||||
appearance="warning"
|
||||
(click)="tryInstall()"
|
||||
@@ -50,6 +53,7 @@ import { MarketplaceAlertsService } from '../services/alerts.service'
|
||||
@case (-1) {
|
||||
<button
|
||||
tuiButton
|
||||
size="m"
|
||||
type="button"
|
||||
appearance="primary"
|
||||
(click)="tryInstall()"
|
||||
@@ -60,6 +64,7 @@ import { MarketplaceAlertsService } from '../services/alerts.service'
|
||||
@case (0) {
|
||||
<button
|
||||
tuiButton
|
||||
size="m"
|
||||
type="button"
|
||||
appearance="secondary-grayscale"
|
||||
(click)="tryInstall()"
|
||||
@@ -71,27 +76,34 @@ import { MarketplaceAlertsService } from '../services/alerts.service'
|
||||
}
|
||||
<button
|
||||
tuiButton
|
||||
size="m"
|
||||
type="button"
|
||||
appearance="secondary-grayscale"
|
||||
(click)="showService()"
|
||||
>
|
||||
{{
|
||||
('View' | i18n) +
|
||||
' ' +
|
||||
($any(local.stateInfo.state | titlecase) | i18n)
|
||||
}}
|
||||
{{ 'View' | i18n }}
|
||||
{{ $any(local.stateInfo.state | titlecase) | i18n }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
tuiButton
|
||||
size="m"
|
||||
type="button"
|
||||
appearance="primary"
|
||||
(click)="tryInstall()"
|
||||
>
|
||||
{{ localFlavor() ? ('Switch' | i18n) : ('Install' | i18n) }}
|
||||
{{ (sameFlavor() ? 'Install' : 'Switch') | i18n }}
|
||||
</button>
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
height: 4.5rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -109,27 +121,35 @@ export class MarketplaceControlsComponent {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly exver = inject(Exver)
|
||||
private readonly router = inject(Router)
|
||||
private readonly marketplaceService = inject(MarketplaceService)
|
||||
private readonly marketplace = inject(MarketplaceService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly preview = inject(MarketplacePreviewComponent)
|
||||
|
||||
version = input.required<string>()
|
||||
installAlert = input.required<string | null>()
|
||||
localPkg = input.required<PackageDataEntry | null>()
|
||||
localFlavor = input.required<boolean>()
|
||||
readonly pkg = input.required<Pick<MarketplacePkg, KEYS>>()
|
||||
|
||||
// only present if side loading
|
||||
file = input<File>()
|
||||
readonly file = input<File>()
|
||||
|
||||
readonly localPkg = toSignal(
|
||||
toObservable(this.pkg).pipe(
|
||||
switchMap(({ id }) => this.patch.watch$('packageData', id)),
|
||||
),
|
||||
)
|
||||
|
||||
readonly sameFlavor = computed(
|
||||
(pkg = this.localPkg()) =>
|
||||
!pkg ||
|
||||
this.exver.getFlavor(getManifest(pkg).version) === this.pkg().flavor,
|
||||
)
|
||||
|
||||
async tryInstall() {
|
||||
const localPkg = this.localPkg()
|
||||
|
||||
const currentUrl = this.file()
|
||||
? null
|
||||
: await firstValueFrom(this.marketplaceService.currentRegistryUrl$)
|
||||
: await firstValueFrom(this.marketplace.currentRegistryUrl$)
|
||||
const originalUrl = localPkg?.registry || null
|
||||
|
||||
if (!localPkg) {
|
||||
if (await this.alerts.alertInstall(this.installAlert() || '')) {
|
||||
if (await this.alerts.alertInstall(this.pkg().alerts.install || '')) {
|
||||
this.installOrUpload(currentUrl)
|
||||
}
|
||||
return
|
||||
@@ -143,11 +163,11 @@ export class MarketplaceControlsComponent {
|
||||
return
|
||||
}
|
||||
|
||||
const localManifest = getManifest(localPkg)
|
||||
const { id, version } = getManifest(localPkg)
|
||||
|
||||
if (
|
||||
hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) &&
|
||||
this.exver.compareExver(localManifest.version, this.version()) !== 0
|
||||
hasCurrentDeps(id, await getAllPackages(this.patch)) &&
|
||||
this.exver.compareExver(version, this.pkg().version) !== 0
|
||||
) {
|
||||
this.dryInstall(currentUrl)
|
||||
} else {
|
||||
@@ -156,16 +176,13 @@ export class MarketplaceControlsComponent {
|
||||
}
|
||||
|
||||
async showService() {
|
||||
this.router.navigate(['/portal/services', this.preview.pkgId])
|
||||
this.router.navigate(['services', this.pkg().id])
|
||||
}
|
||||
|
||||
private async dryInstall(url: string | null) {
|
||||
const id = this.preview.pkgId
|
||||
const breakages = dryUpdate(
|
||||
{ id, version: this.version() },
|
||||
await getAllPackages(this.patch),
|
||||
this.exver,
|
||||
)
|
||||
const { id, version } = this.pkg()
|
||||
const packages = await getAllPackages(this.patch)
|
||||
const breakages = dryUpdate({ id, version }, packages, this.exver)
|
||||
|
||||
if (
|
||||
isEmptyObject(breakages) ||
|
||||
@@ -178,7 +195,7 @@ export class MarketplaceControlsComponent {
|
||||
private async installOrUpload(url: string | null) {
|
||||
if (this.file()) {
|
||||
await this.upload()
|
||||
this.router.navigate(['/portal', 'services'])
|
||||
this.router.navigate(['services'])
|
||||
} else if (url) {
|
||||
await this.install(url)
|
||||
}
|
||||
@@ -186,10 +203,10 @@ export class MarketplaceControlsComponent {
|
||||
|
||||
private async install(url: string) {
|
||||
const loader = this.loader.open('Beginning install').subscribe()
|
||||
const id = this.preview.pkgId
|
||||
const { id, version } = this.pkg()
|
||||
|
||||
try {
|
||||
await this.marketplaceService.installPackage(id, this.version(), url)
|
||||
await this.marketplace.installPackage(id, version, url)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
|
||||
@@ -6,30 +6,15 @@ import {
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { toObservable, toSignal } from '@angular/core/rxjs-interop'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { ItemModule, MarketplacePkg } from '@start9labs/marketplace'
|
||||
import { Exver } from '@start9labs/shared'
|
||||
import { TuiAutoFocus } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiDropdownService, TuiPopup } from '@taiga-ui/core'
|
||||
import { TuiDrawer } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
debounceTime,
|
||||
filter,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
} from 'rxjs'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { debounceTime } from 'rxjs'
|
||||
import { MarketplacePreviewComponent } from '../modals/preview.component'
|
||||
import { MarketplaceSidebarService } from '../services/sidebar.service'
|
||||
import { MarketplaceControlsComponent } from './controls.component'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-tile',
|
||||
@@ -40,7 +25,7 @@ import { MarketplaceControlsComponent } from './controls.component'
|
||||
[overlay]="true"
|
||||
(click.self)="toggle(false)"
|
||||
>
|
||||
<marketplace-preview [pkgId]="pkg().id" class="preview-wrapper">
|
||||
<marketplace-preview [pkgId]="pkg().id">
|
||||
<button
|
||||
tuiAutoFocus
|
||||
slot="close"
|
||||
@@ -53,19 +38,18 @@ import { MarketplaceControlsComponent } from './controls.component'
|
||||
[tuiAppearanceFocus]="false"
|
||||
(click)="toggle(false)"
|
||||
></button>
|
||||
<marketplace-controls
|
||||
slot="controls"
|
||||
class="controls-wrapper"
|
||||
[version]="pkg().version"
|
||||
[installAlert]="pkg().alerts.install"
|
||||
[localPkg]="local$ | async"
|
||||
[localFlavor]="!!(flavor$ | async)"
|
||||
/>
|
||||
</marketplace-preview>
|
||||
</tui-drawer>
|
||||
</marketplace-item>
|
||||
`,
|
||||
styles: `
|
||||
@keyframes animateIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.6) translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
:host {
|
||||
cursor: pointer;
|
||||
animation: animateIn 400ms calc(var(--animation-order) * 200ms) both;
|
||||
@@ -77,28 +61,7 @@ import { MarketplaceControlsComponent } from './controls.component'
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@keyframes animateIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.6) translateY(-20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-wrapper {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-width: 30rem;
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
button {
|
||||
place-self: end;
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -106,13 +69,6 @@ import { MarketplaceControlsComponent } from './controls.component'
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.controls-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
height: 4.5rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [
|
||||
@@ -128,13 +84,10 @@ import { MarketplaceControlsComponent } from './controls.component'
|
||||
TuiButton,
|
||||
TuiPopup,
|
||||
TuiDrawer,
|
||||
MarketplaceControlsComponent,
|
||||
MarketplacePreviewComponent,
|
||||
],
|
||||
})
|
||||
export class MarketplaceTileComponent {
|
||||
private readonly exver = inject(Exver)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly router = inject(Router)
|
||||
private readonly params = toSignal(
|
||||
inject(ActivatedRoute).queryParamMap.pipe(debounceTime(100)),
|
||||
@@ -147,24 +100,6 @@ export class MarketplaceTileComponent {
|
||||
this.params()?.get('flavor') === this.pkg()?.flavor,
|
||||
)
|
||||
|
||||
readonly local$: Observable<PackageDataEntry | null> = toObservable(
|
||||
this.pkg,
|
||||
).pipe(
|
||||
switchMap(({ id, flavor }) =>
|
||||
this.patch.watch$('packageData', id).pipe(
|
||||
filter(Boolean),
|
||||
map(pkg =>
|
||||
this.exver.getFlavor(getManifest(pkg).version) === flavor
|
||||
? pkg
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
readonly flavor$ = this.local$.pipe(map(pkg => !pkg))
|
||||
|
||||
toggle(open: boolean) {
|
||||
this.router.navigate([], {
|
||||
queryParams: {
|
||||
|
||||
@@ -9,10 +9,8 @@ import {
|
||||
} from '@start9labs/marketplace'
|
||||
import { defaultRegistries, i18nPipe } from '@start9labs/shared'
|
||||
import { TuiScrollbar } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { tap, withLatestFrom } from 'rxjs'
|
||||
import { tap } from 'rxjs'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { MarketplaceMenuComponent } from './components/menu.component'
|
||||
import { MarketplaceNotificationComponent } from './components/notification.component'
|
||||
|
||||
@@ -3,29 +3,26 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
Input,
|
||||
TemplateRef,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
AboutModule,
|
||||
AdditionalModule,
|
||||
FlavorsComponent,
|
||||
MarketplaceAdditionalItemComponent,
|
||||
MarketplaceLinksComponent,
|
||||
MarketplaceFlavorsComponent,
|
||||
MarketplaceAboutComponent,
|
||||
MarketplaceDependenciesComponent,
|
||||
MarketplacePackageHeroComponent,
|
||||
MarketplacePkg,
|
||||
MarketplaceVersionsComponent,
|
||||
MarketplaceReleaseNotesComponent,
|
||||
} from '@start9labs/marketplace'
|
||||
import {
|
||||
DialogService,
|
||||
Exver,
|
||||
i18nPipe,
|
||||
MARKDOWN,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import { TuiButton, TuiDialogContext, TuiLoader } from '@taiga-ui/core'
|
||||
import { TuiRadioList } from '@taiga-ui/kit'
|
||||
import { TuiLoader } from '@taiga-ui/core'
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
@@ -34,7 +31,9 @@ import {
|
||||
startWith,
|
||||
switchMap,
|
||||
} from 'rxjs'
|
||||
import { shareReplay, take, tap } from 'rxjs/operators'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { MarketplaceControlsComponent } from '../components/controls.component'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-preview',
|
||||
@@ -43,50 +42,25 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
<ng-content select="[slot=close]" />
|
||||
@if (pkg$ | async; as pkg) {
|
||||
<marketplace-package-hero [pkg]="pkg">
|
||||
<ng-content select="[slot=controls]" />
|
||||
<marketplace-controls [pkg]="pkg" />
|
||||
</marketplace-package-hero>
|
||||
<div class="inner-container">
|
||||
<marketplace-about [pkg]="pkg" (static)="onStatic()" />
|
||||
<marketplace-release-notes [pkg]="pkg" />
|
||||
@if (flavors$ | async; as flavors) {
|
||||
<marketplace-flavors [pkgs]="flavors" />
|
||||
}
|
||||
<marketplace-about [pkg]="pkg" />
|
||||
@if (!(pkg.dependencyMetadata | empty)) {
|
||||
<marketplace-dependencies [pkg]="pkg" (open)="open($event)" />
|
||||
}
|
||||
<marketplace-additional [pkg]="pkg" (static)="onStatic($event)">
|
||||
@if (versions$ | async; as versions) {
|
||||
<marketplace-additional-item
|
||||
(click)="selectVersion(pkg, version)"
|
||||
[data]="('Click to view all versions' | i18n) || ''"
|
||||
icon="@tui.chevron-right"
|
||||
label="All versions"
|
||||
class="versions"
|
||||
/>
|
||||
<ng-template
|
||||
#version
|
||||
let-data="data"
|
||||
let-completeWith="completeWith"
|
||||
>
|
||||
<tui-radio-list [items]="versions" [(ngModel)]="data.version" />
|
||||
<footer class="buttons">
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
(click)="completeWith(null)"
|
||||
>
|
||||
{{ 'Cancel' | i18n }}
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary"
|
||||
(click)="completeWith(data.version)"
|
||||
>
|
||||
{{ 'Ok' | i18n }}
|
||||
</button>
|
||||
</footer>
|
||||
</ng-template>
|
||||
}
|
||||
</marketplace-additional>
|
||||
<marketplace-links [pkg]="pkg" />
|
||||
@if (versions$ | async; as versions) {
|
||||
<marketplace-versions
|
||||
[version]="pkg.version"
|
||||
[versions]="versions"
|
||||
(onVersion)="selectedVersion$.next($event)"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<tui-loader textContent="Loading" [style.height.%]="100" />
|
||||
@@ -96,6 +70,13 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
styles: `
|
||||
:host {
|
||||
pointer-events: auto;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-width: 30rem;
|
||||
}
|
||||
}
|
||||
|
||||
.outer-container {
|
||||
@@ -125,20 +106,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
}
|
||||
}
|
||||
|
||||
.versions {
|
||||
border: 0;
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
border-color: rgb(113 113 122);
|
||||
border-style: solid;
|
||||
cursor: pointer;
|
||||
|
||||
::ng-deep label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
marketplace-additional {
|
||||
marketplace-versions {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
`,
|
||||
@@ -147,37 +115,39 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
CommonModule,
|
||||
MarketplacePackageHeroComponent,
|
||||
MarketplaceDependenciesComponent,
|
||||
MarketplaceAdditionalItemComponent,
|
||||
TuiButton,
|
||||
AdditionalModule,
|
||||
AboutModule,
|
||||
SharedPipesModule,
|
||||
FormsModule,
|
||||
TuiRadioList,
|
||||
TuiLoader,
|
||||
FlavorsComponent,
|
||||
i18nPipe,
|
||||
MarketplaceLinksComponent,
|
||||
MarketplaceFlavorsComponent,
|
||||
MarketplaceAboutComponent,
|
||||
MarketplaceControlsComponent,
|
||||
MarketplaceVersionsComponent,
|
||||
MarketplaceReleaseNotesComponent,
|
||||
],
|
||||
})
|
||||
export class MarketplacePreviewComponent {
|
||||
@Input({ required: true })
|
||||
pkgId!: string
|
||||
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly exver = inject(Exver)
|
||||
private readonly router = inject(Router)
|
||||
private readonly marketplaceService = inject(MarketplaceService)
|
||||
|
||||
readonly pkgId = input.required<string>()
|
||||
|
||||
private readonly flavor$ = this.router.routerState.root.queryParamMap.pipe(
|
||||
map(paramMap => paramMap.get('flavor')),
|
||||
take(1),
|
||||
)
|
||||
|
||||
readonly version$ = new BehaviorSubject<string | null>(null)
|
||||
readonly pkg$ = combineLatest([this.version$, this.flavor$]).pipe(
|
||||
readonly selectedVersion$ = new BehaviorSubject<string | null>(null)
|
||||
|
||||
readonly pkg$ = combineLatest([this.selectedVersion$, this.flavor$]).pipe(
|
||||
tap(console.error),
|
||||
switchMap(([version, flavor]) =>
|
||||
this.marketplaceService
|
||||
.getPackage$(this.pkgId, version, flavor)
|
||||
.getPackage$(this.pkgId(), version, flavor)
|
||||
.pipe(startWith(null)),
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: false }),
|
||||
)
|
||||
|
||||
readonly flavors$ = this.flavor$.pipe(
|
||||
@@ -185,7 +155,7 @@ export class MarketplacePreviewComponent {
|
||||
this.marketplaceService.currentRegistry$.pipe(
|
||||
map(({ packages }) =>
|
||||
packages.filter(
|
||||
({ id, flavor }) => id === this.pkgId && flavor !== current,
|
||||
({ id, flavor }) => id === this.pkgId() && flavor !== current,
|
||||
),
|
||||
),
|
||||
filter(p => p.length > 0),
|
||||
@@ -211,34 +181,18 @@ export class MarketplacePreviewComponent {
|
||||
this.router.navigate([], { queryParams: { id } })
|
||||
}
|
||||
|
||||
onStatic(asset: 'license' | 'instructions') {
|
||||
const label = asset === 'license' ? 'License' : 'Instructions'
|
||||
onStatic() {
|
||||
const content = this.pkg$.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(pkg =>
|
||||
this.marketplaceService.fetchStatic$(
|
||||
pkg,
|
||||
asset === 'license' ? 'LICENSE.md' : 'instructions.md',
|
||||
),
|
||||
),
|
||||
switchMap(pkg => this.marketplaceService.fetchStatic$(pkg)),
|
||||
)
|
||||
|
||||
this.dialog
|
||||
.openComponent(MARKDOWN, { label, size: 'l', data: { content } })
|
||||
.openComponent(MARKDOWN, {
|
||||
label: 'License',
|
||||
size: 'l',
|
||||
data: { content },
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
selectVersion(
|
||||
{ version }: MarketplacePkg,
|
||||
template: TemplateRef<TuiDialogContext>,
|
||||
) {
|
||||
this.dialog
|
||||
.openComponent<string>(template, {
|
||||
label: 'All versions',
|
||||
size: 's',
|
||||
data: { version },
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(selected => this.version$.next(selected))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export class MarketplaceAlertsService {
|
||||
|
||||
async alertBreakages(breakages: string[]): Promise<boolean> {
|
||||
let content =
|
||||
`${this.i18n.transform('As a result of this update, the following services will no longer work properly and may crash')}:<ul>'` as i18nKey
|
||||
`${this.i18n.transform('As a result of this update, the following services will no longer work properly and may crash')}:<ul>` as i18nKey
|
||||
const bullets = breakages.map(title => `<li><b>${title}</b></li>`)
|
||||
content = `${content}${bullets.join('')}</ul>` as i18nKey
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { map } from 'rxjs'
|
||||
import { ControlsService } from 'src/app/services/controls.service'
|
||||
@@ -41,11 +40,11 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
}
|
||||
|
||||
@if (status() === 'stopped') {
|
||||
@let unmet = hasUnmet() | async;
|
||||
<button
|
||||
*tuiLet="hasUnmet() | async as hasUnmet"
|
||||
tuiButton
|
||||
iconStart="@tui.play"
|
||||
(click)="controls.start(manifest(), !!hasUnmet)"
|
||||
(click)="controls.start(manifest(), !!unmet)"
|
||||
>
|
||||
{{ 'Start' | i18n }}
|
||||
</button>
|
||||
@@ -83,7 +82,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, i18nPipe, TuiLet, AsyncPipe],
|
||||
imports: [TuiButton, i18nPipe, AsyncPipe],
|
||||
})
|
||||
export class ServiceControlsComponent {
|
||||
private readonly errors = inject(DepErrorService)
|
||||
|
||||
@@ -16,7 +16,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
@for (d of pkg.currentDependencies | keyvalue; track $index) {
|
||||
<a
|
||||
tuiCell
|
||||
[routerLink]="services[d.key] ? ['..', d.key] : ['/portal/marketplace']"
|
||||
[routerLink]="services[d.key] ? ['..', d.key] : ['/marketplace']"
|
||||
[queryParams]="services[d.key] ? {} : { id: d.key }"
|
||||
[class.error]="getError(d.key)"
|
||||
>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
DOCUMENT,
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
} from '@angular/core'
|
||||
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { TuiProgress } from '@taiga-ui/kit'
|
||||
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
|
||||
@@ -34,7 +33,8 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
phase of pkg.stateInfo.installingInfo?.progress?.phases;
|
||||
track $index
|
||||
) {
|
||||
<div *tuiLet="phase.progress | installingProgress as percent">
|
||||
@let percent = phase.progress | installingProgress;
|
||||
<div>
|
||||
{{ $any(phase.name) | i18n }}:
|
||||
@if (phase.progress === null) {
|
||||
<span>{{ 'waiting' | i18n }}</span>
|
||||
@@ -76,7 +76,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiProgress, TuiLet, InstallingProgressPipe, i18nPipe, TuiButton],
|
||||
imports: [TuiProgress, InstallingProgressPipe, i18nPipe, TuiButton],
|
||||
})
|
||||
export class ServiceInstallProgressComponent {
|
||||
@Input({ required: true })
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TuiButton, tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { map } from 'rxjs'
|
||||
import { ControlsService } from 'src/app/services/controls.service'
|
||||
@@ -15,7 +15,6 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { UILaunchComponent } from './ui-launch.component'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
|
||||
const RUNNING = ['running', 'starting', 'restarting']
|
||||
|
||||
@@ -32,19 +31,19 @@ const RUNNING = ['running', 'starting', 'restarting']
|
||||
{{ 'Stop' | i18n }}
|
||||
</button>
|
||||
} @else {
|
||||
@let unmet = hasUnmet() | async;
|
||||
<button
|
||||
*tuiLet="hasUnmet() | async as hasUnmet"
|
||||
tuiIconButton
|
||||
iconStart="@tui.play"
|
||||
[disabled]="status().primary !== 'stopped'"
|
||||
(click)="controls.start(manifest(), !!hasUnmet)"
|
||||
(click)="controls.start(manifest(), !!unmet)"
|
||||
>
|
||||
{{ 'Start' | i18n }}
|
||||
</button>
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiButton, UILaunchComponent, TuiLet, AsyncPipe, i18nPipe],
|
||||
imports: [TuiButton, UILaunchComponent, AsyncPipe, i18nPipe],
|
||||
providers: [tuiButtonOptionsProvider({ size: 's', appearance: 'none' })],
|
||||
styles: `
|
||||
:host {
|
||||
|
||||
@@ -162,7 +162,7 @@ export class ServiceComponent implements OnChanges {
|
||||
}
|
||||
|
||||
get routerLink() {
|
||||
return `/portal/services/${this.manifest.id}`
|
||||
return `/services/${this.manifest.id}`
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
DOCUMENT,
|
||||
} from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
@@ -82,12 +82,8 @@ export default class ServiceAboutRoute {
|
||||
action: () => this.copyService.copy(manifest.version),
|
||||
},
|
||||
{
|
||||
name: 'SDK Version',
|
||||
value: manifest.sdkVersion || '-',
|
||||
icon: manifest.sdkVersion ? '@tui.copy' : '',
|
||||
action: () =>
|
||||
manifest.sdkVersion &&
|
||||
this.copyService.copy(manifest.sdkVersion),
|
||||
name: 'Installed From',
|
||||
value: pkg.registry || NOT_PROVIDED,
|
||||
},
|
||||
{
|
||||
name: 'Git Hash',
|
||||
@@ -96,6 +92,14 @@ export default class ServiceAboutRoute {
|
||||
action: () =>
|
||||
manifest.gitHash && this.copyService.copy(manifest.gitHash),
|
||||
},
|
||||
{
|
||||
name: 'SDK Version',
|
||||
value: manifest.sdkVersion || '-',
|
||||
icon: manifest.sdkVersion ? '@tui.copy' : '',
|
||||
action: () =>
|
||||
manifest.sdkVersion &&
|
||||
this.copyService.copy(manifest.sdkVersion),
|
||||
},
|
||||
{
|
||||
name: 'License',
|
||||
value: manifest.license,
|
||||
@@ -105,30 +109,35 @@ export default class ServiceAboutRoute {
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Links',
|
||||
header: 'Source Code',
|
||||
items: [
|
||||
{
|
||||
name: 'Installed From',
|
||||
value: pkg.registry || NOT_PROVIDED,
|
||||
},
|
||||
{
|
||||
name: 'Service Repository',
|
||||
name: 'Upstream service',
|
||||
value: manifest.upstreamRepo,
|
||||
},
|
||||
{
|
||||
name: 'Package Repository',
|
||||
name: 'StartOS package',
|
||||
value: manifest.wrapperRepo,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Links',
|
||||
items: [
|
||||
{
|
||||
name: 'Marketing Site',
|
||||
name: 'Marketing',
|
||||
value: manifest.marketingSite || NOT_PROVIDED,
|
||||
},
|
||||
{
|
||||
name: 'Support Site',
|
||||
name: 'Documentation',
|
||||
value: manifest.docsUrl || NOT_PROVIDED,
|
||||
},
|
||||
{
|
||||
name: 'Support',
|
||||
value: manifest.supportSite || NOT_PROVIDED,
|
||||
},
|
||||
{
|
||||
name: 'Donation Link',
|
||||
name: 'Donations',
|
||||
value: manifest.donationUrl || NOT_PROVIDED,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { KeyValuePipe } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { getPkgId, i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
@@ -12,29 +17,24 @@ import { StandardActionsService } from 'src/app/services/standard-actions.servic
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { ServiceActionComponent } from '../components/action.component'
|
||||
|
||||
const OTHER = 'Custom Actions'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@if (package(); as pkg) {
|
||||
@for (group of pkg.actions | keyvalue; track $index) {
|
||||
@if (group.value.length) {
|
||||
<section class="g-card">
|
||||
<header>{{ group.key }}</header>
|
||||
@for (a of group.value; track $index) {
|
||||
@if (a.visibility !== 'hidden') {
|
||||
<button
|
||||
tuiCell
|
||||
[action]="a"
|
||||
(click)="handle(pkg.mainStatus, pkg.icon, pkg.manifest, a)"
|
||||
></button>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
}
|
||||
<section class="g-card">
|
||||
<header>{{ group.key }}</header>
|
||||
@for (a of group.value; track $index) {
|
||||
<button
|
||||
tuiCell
|
||||
[action]="a"
|
||||
(click)="handle(pkg.mainStatus, pkg.icon, pkg.manifest, a)"
|
||||
></button>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
<section class="g-card">
|
||||
<header>{{ 'Standard Actions' | i18n }}</header>
|
||||
<header>StartOS</header>
|
||||
<button
|
||||
tuiCell
|
||||
[action]="rebuild"
|
||||
@@ -58,37 +58,56 @@ const OTHER = 'Custom Actions'
|
||||
`,
|
||||
host: { class: 'g-subpage' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ServiceActionComponent, TuiCell, KeyValuePipe, i18nPipe],
|
||||
imports: [ServiceActionComponent, TuiCell, KeyValuePipe],
|
||||
})
|
||||
export default class ServiceActionsRoute {
|
||||
private readonly actions = inject(ActionService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
ungrouped: 'General' | 'Other' = 'General'
|
||||
|
||||
readonly service = inject(StandardActionsService)
|
||||
readonly package = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('packageData', getPkgId())
|
||||
.pipe(
|
||||
filter(pkg => pkg.stateInfo.state === 'installed'),
|
||||
map(pkg => ({
|
||||
mainStatus: pkg.status.main,
|
||||
icon: pkg.icon,
|
||||
manifest: getManifest(pkg),
|
||||
actions: Object.entries(pkg.actions)
|
||||
.filter(([_, val]) => val.visibility !== 'hidden')
|
||||
.reduce<
|
||||
Record<string, ReadonlyArray<T.ActionMetadata & { id: string }>>
|
||||
>(
|
||||
(acc, [id]) => {
|
||||
const action = { id, ...pkg.actions[id]! }
|
||||
const group = pkg.actions[id]?.group || OTHER
|
||||
const current = acc[group] || []
|
||||
|
||||
return { ...acc, [group]: current.concat(action) }
|
||||
},
|
||||
{ [OTHER]: [] },
|
||||
),
|
||||
})),
|
||||
map(pkg => {
|
||||
const specialGroup = Object.values(pkg.actions).some(
|
||||
pkg => !!pkg.group,
|
||||
)
|
||||
? 'Other'
|
||||
: 'General'
|
||||
return {
|
||||
mainStatus: pkg.status.main,
|
||||
icon: pkg.icon,
|
||||
manifest: getManifest(pkg),
|
||||
actions: Object.entries(pkg.actions)
|
||||
.map(([id, action]) => ({
|
||||
...action,
|
||||
id,
|
||||
group: action.group || specialGroup,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.group === specialGroup) return 1
|
||||
if (b.group === specialGroup) return -1
|
||||
return a.group.localeCompare(b.group) // Optional: sort others alphabetically
|
||||
})
|
||||
.reduce<
|
||||
Record<
|
||||
string,
|
||||
Array<T.ActionMetadata & { id: string; group: string }>
|
||||
>
|
||||
>((acc, action) => {
|
||||
const key = action.group
|
||||
if (!acc[key]) {
|
||||
acc[key] = []
|
||||
}
|
||||
acc[key].push(action)
|
||||
return acc
|
||||
}, {}),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -102,7 +121,7 @@ export default class ServiceActionsRoute {
|
||||
readonly uninstall = {
|
||||
name: this.i18n.transform('Uninstall')!,
|
||||
description: this.i18n.transform(
|
||||
'Uninstalls this service from StartOS and delete all data permanently.',
|
||||
'Uninstalls this service from StartOS and deletes all data permanently.',
|
||||
)!,
|
||||
}
|
||||
|
||||
|
||||
@@ -33,13 +33,15 @@ const INACTIVE: PrimaryStatus[] = [
|
||||
@if (service()) {
|
||||
<div *title class="title">
|
||||
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||
<tui-avatar size="xs" [style.margin-inline-end.rem]="0.75">
|
||||
<img alt="" [src]="service()?.icon" />
|
||||
</tui-avatar>
|
||||
<span tuiFade>{{ manifest()?.title }}</span>
|
||||
<div routerLink="./" class="m-header">
|
||||
<tui-avatar size="xs" [style.margin-inline-end.rem]="0.75">
|
||||
<img alt="" [src]="service()?.icon" />
|
||||
</tui-avatar>
|
||||
<span tuiFade>{{ manifest()?.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="g-aside">
|
||||
<header tuiCell>
|
||||
<header tuiCell routerLink="./">
|
||||
<tui-avatar><img alt="" [src]="service()?.icon" /></tui-avatar>
|
||||
<span tuiTitle>
|
||||
<strong tuiFade>{{ manifest()?.title }}</strong>
|
||||
@@ -88,6 +90,7 @@ const INACTIVE: PrimaryStatus[] = [
|
||||
|
||||
header {
|
||||
margin: 0 -0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
nav[inert] a:not(:first-child) {
|
||||
@@ -107,6 +110,11 @@ const INACTIVE: PrimaryStatus[] = [
|
||||
}
|
||||
}
|
||||
|
||||
.m-header {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
@@ -171,7 +179,6 @@ export class ServiceOutletComponent {
|
||||
protected readonly nav: { title: i18nKey; icon: string }[] = [
|
||||
{ title: 'dashboard', icon: '@tui.layout-dashboard' },
|
||||
{ title: 'actions', icon: '@tui.clapperboard' },
|
||||
{ title: 'instructions', icon: '@tui.book-open-text' },
|
||||
{ title: 'logs', icon: '@tui.logs' },
|
||||
{ title: 'about', icon: '@tui.info' },
|
||||
]
|
||||
@@ -186,7 +193,7 @@ export class ServiceOutletComponent {
|
||||
tap(pkg => {
|
||||
// if package disappears, navigate to list page
|
||||
if (!pkg) {
|
||||
this.router.navigate(['./portal/services'])
|
||||
this.router.navigate(['services'])
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { inject } from '@angular/core'
|
||||
import { ActivatedRouteSnapshot, ResolveFn, Routes } from '@angular/router'
|
||||
import { MarkdownComponent } from '@start9labs/shared'
|
||||
import { defer, map, Observable, of } from 'rxjs'
|
||||
import { share } from 'rxjs/operators'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
@@ -23,20 +22,6 @@ export const ROUTES: Routes = [
|
||||
path: 'actions',
|
||||
loadComponent: () => import('./routes/actions.component'),
|
||||
},
|
||||
{
|
||||
path: 'instructions',
|
||||
component: MarkdownComponent,
|
||||
resolve: { content: getStatic('instructions.md') },
|
||||
canActivate: [
|
||||
({ paramMap }: ActivatedRouteSnapshot) => {
|
||||
inject(ApiService)
|
||||
.setDbValue(['ackInstructions', paramMap.get('pkgId')!], true)
|
||||
.catch(e => console.error('Failed to mark as seen', e))
|
||||
|
||||
return true
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'interface/:interfaceId',
|
||||
loadComponent: () => import('./routes/interface.component'),
|
||||
@@ -48,7 +33,7 @@ export const ROUTES: Routes = [
|
||||
{
|
||||
path: 'about',
|
||||
loadComponent: () => import('./routes/about.component'),
|
||||
resolve: { content: getStatic('LICENSE.md') },
|
||||
resolve: { content: getStatic() },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -59,15 +44,13 @@ export const ROUTES: Routes = [
|
||||
},
|
||||
]
|
||||
|
||||
function getStatic(
|
||||
path: 'LICENSE.md' | 'instructions.md',
|
||||
): ResolveFn<Observable<string>> {
|
||||
function getStatic(): ResolveFn<Observable<string>> {
|
||||
return ({ paramMap }: ActivatedRouteSnapshot) =>
|
||||
of(inject(ApiService)).pipe(
|
||||
map(api =>
|
||||
defer(() => api.getStaticInstalled(paramMap.get('pkgId')!, path)).pipe(
|
||||
share(),
|
||||
),
|
||||
defer(() =>
|
||||
api.getStaticInstalled(paramMap.get('pkgId')!, 'LICENSE.md'),
|
||||
).pipe(share()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, input } from '@angular/core'
|
||||
import { toObservable } from '@angular/core/rxjs-interop'
|
||||
import {
|
||||
AboutModule,
|
||||
AdditionalModule,
|
||||
MarketplaceAboutComponent,
|
||||
MarketplaceLinksComponent,
|
||||
MarketplaceDependenciesComponent,
|
||||
MarketplacePackageHeroComponent,
|
||||
MarketplaceReleaseNotesComponent,
|
||||
} from '@start9labs/marketplace'
|
||||
import {
|
||||
DialogService,
|
||||
Exver,
|
||||
MARKDOWN,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, first, map, of, switchMap } from 'rxjs'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { DialogService, MARKDOWN, SharedPipesModule } from '@start9labs/shared'
|
||||
import { of } from 'rxjs'
|
||||
import { MarketplaceControlsComponent } from '../marketplace/components/controls.component'
|
||||
import { MarketplacePkgSideload } from './sideload.utils'
|
||||
|
||||
@@ -26,25 +18,18 @@ import { MarketplacePkgSideload } from './sideload.utils'
|
||||
<div class="outer-container">
|
||||
<ng-content />
|
||||
<marketplace-package-hero [pkg]="pkg()">
|
||||
<marketplace-controls
|
||||
slot="controls"
|
||||
class="controls-wrapper"
|
||||
[version]="pkg().version"
|
||||
[installAlert]="pkg().alerts.install"
|
||||
[localPkg]="local$ | async"
|
||||
[localFlavor]="!!(flavor$ | async)"
|
||||
[file]="file()"
|
||||
/>
|
||||
<marketplace-controls [pkg]="pkg()" [file]="file()" />
|
||||
</marketplace-package-hero>
|
||||
<div class="package-details">
|
||||
<div class="package-details-main">
|
||||
<marketplace-about [pkg]="pkg()" />
|
||||
<marketplace-release-notes [pkg]="pkg()" />
|
||||
@if (!(pkg().dependencyMetadata | empty)) {
|
||||
<marketplace-dependencies [pkg]="pkg()" />
|
||||
}
|
||||
</div>
|
||||
<div class="package-details-additional">
|
||||
<marketplace-additional [pkg]="pkg()" (static)="onStatic($event)" />
|
||||
<marketplace-links [pkg]="pkg()" (static)="onStatic()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,7 +47,6 @@ import { MarketplacePkgSideload } from './sideload.utils'
|
||||
}
|
||||
|
||||
.package-details {
|
||||
-moz-column-gap: 2rem;
|
||||
column-gap: 2rem;
|
||||
|
||||
&-main {
|
||||
@@ -80,62 +64,36 @@ import { MarketplacePkgSideload } from './sideload.utils'
|
||||
}
|
||||
&-additional {
|
||||
grid-column: span 4 / span 4;
|
||||
margin-top: 0px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controls-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
height: 4.5rem;
|
||||
}
|
||||
`,
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedPipesModule,
|
||||
AboutModule,
|
||||
AdditionalModule,
|
||||
MarketplaceAboutComponent,
|
||||
MarketplaceLinksComponent,
|
||||
MarketplacePackageHeroComponent,
|
||||
MarketplaceDependenciesComponent,
|
||||
MarketplaceControlsComponent,
|
||||
MarketplaceReleaseNotesComponent,
|
||||
],
|
||||
})
|
||||
export class SideloadPackageComponent {
|
||||
private readonly exver = inject(Exver)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly dialog = inject(DialogService)
|
||||
|
||||
readonly pkg = input.required<MarketplacePkgSideload>()
|
||||
readonly file = input.required<File>()
|
||||
|
||||
readonly local$ = toObservable(this.pkg).pipe(
|
||||
filter(Boolean),
|
||||
switchMap(({ id, flavor }) =>
|
||||
this.patch.watch$('packageData', id).pipe(
|
||||
filter(Boolean),
|
||||
map(pkg =>
|
||||
this.exver.getFlavor(getManifest(pkg).version) === flavor
|
||||
? pkg
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
first(),
|
||||
)
|
||||
|
||||
readonly flavor$ = this.local$.pipe(map(pkg => !pkg))
|
||||
|
||||
onStatic(type: 'license' | 'instructions') {
|
||||
const label = type === 'license' ? 'License' : 'Instructions'
|
||||
const key = type === 'license' ? 'fullLicense' : 'instructions'
|
||||
onStatic() {
|
||||
const content = of(this.pkg()['license'])
|
||||
|
||||
this.dialog
|
||||
.openComponent(MARKDOWN, {
|
||||
label,
|
||||
label: 'License',
|
||||
size: 'l',
|
||||
data: { content: of(this.pkg()[key]) },
|
||||
data: { content },
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
import { tuiIsString } from '@taiga-ui/cdk'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import {
|
||||
@@ -14,9 +15,9 @@ import {
|
||||
} from '@taiga-ui/kit'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
|
||||
import { SideloadPackageComponent } from './package.component'
|
||||
import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
|
||||
import { i18nKey, i18nPipe } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
|
||||
@@ -37,12 +37,10 @@ async function parseS9pk(file: File): Promise<MarketplacePkgSideload> {
|
||||
return {
|
||||
...s9pk.manifest,
|
||||
dependencyMetadata: await s9pk.dependencyMetadata(),
|
||||
gitHash: '',
|
||||
icon: await s9pk.icon(),
|
||||
sourceVersion: s9pk.manifest.canMigrateFrom,
|
||||
flavor: ExtendedVersion.parse(s9pk.manifest.version).flavor,
|
||||
fullLicense: await s9pk.license(),
|
||||
instructions: await s9pk.instructions(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +75,5 @@ function compare(a: Uint8Array, b: Uint8Array) {
|
||||
}
|
||||
|
||||
export type MarketplacePkgSideload = MarketplacePkgBase & {
|
||||
instructions: string
|
||||
fullLicense: string
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user