mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Misc frontend fixes (#2974)
* fix dependency input warning and extra comma * clean up buttons during install in marketplace preview * chore: grayscale and closing action-bar * fix prerelease precedence * fix duplicate url for addSsl on ssl proto * no warning for soft uninstall * fix: stop logs from repeating disconnected status and add 1 second delay between reconnection attempts * fix stop on reactivation of critical task * fix: fix disconnected toast * fix: updates styles * fix: updates styles * misc fixes * beta.33 * fix updates badge and initialization of marketplace preview controls --------- Co-authored-by: waterplea <alexander@inkin.ru> Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
2
container-runtime/package-lock.json
generated
2
container-runtime/package-lock.json
generated
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"../sdk/dist": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.32",
|
||||
"version": "0.4.0-beta.33",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0",
|
||||
|
||||
@@ -394,7 +394,9 @@ impl RpcContext {
|
||||
if let Some(service) = self.services.get(&package_id).await.as_ref() {
|
||||
if let Some(input) = service
|
||||
.get_action_input(procedure_id.clone(), action_id.clone())
|
||||
.await?
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
.and_then(|i| i.value)
|
||||
{
|
||||
action_input
|
||||
|
||||
@@ -38,7 +38,7 @@ use crate::rpc_continuations::{Guid, RpcContinuation};
|
||||
use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL};
|
||||
use crate::ssh::SSH_DIR;
|
||||
use crate::system::{get_mem_info, sync_kiosk};
|
||||
use crate::util::io::{create_file, IOHook};
|
||||
use crate::util::io::{create_file, open_file, IOHook};
|
||||
use crate::util::lshw::lshw;
|
||||
use crate::util::net::WebSocketExt;
|
||||
use crate::util::{cpupower, Invoke};
|
||||
@@ -399,6 +399,11 @@ pub async fn init(
|
||||
.invoke(crate::ErrorKind::Journald)
|
||||
.await?;
|
||||
mount_logs.complete();
|
||||
tokio::io::copy(
|
||||
&mut open_file("/run/startos/init.log").await?,
|
||||
&mut tokio::io::stderr(),
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("Mounted Logs");
|
||||
|
||||
load_ca_cert.start();
|
||||
|
||||
@@ -89,8 +89,13 @@ impl LxcManager {
|
||||
log_mount: Option<&Path>,
|
||||
config: LxcConfig,
|
||||
) -> Result<LxcContainer, Error> {
|
||||
let container = LxcContainer::new(self, log_mount, config).await?;
|
||||
let mut guard = self.containers.lock().await;
|
||||
let container = tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
LxcContainer::new(self, log_mount, config),
|
||||
)
|
||||
.await
|
||||
.with_kind(ErrorKind::Timeout)??;
|
||||
*guard = std::mem::take(&mut *guard)
|
||||
.into_iter()
|
||||
.filter(|g| g.strong_count() > 0)
|
||||
@@ -223,6 +228,17 @@ impl LxcContainer {
|
||||
.arg(&log_mount_point)
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?;
|
||||
match Command::new("chattr")
|
||||
.arg("-R")
|
||||
.arg("+C")
|
||||
.arg(&log_mount_point)
|
||||
.invoke(ErrorKind::Filesystem)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) if e.source.to_string().contains("Operation not supported") => Ok(()),
|
||||
Err(e) => Err(e),
|
||||
}?;
|
||||
Some(log_mount)
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -62,12 +62,14 @@ impl BindInfo {
|
||||
pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result<Self, Error> {
|
||||
let mut assigned_port = None;
|
||||
let mut assigned_ssl_port = None;
|
||||
if options.secure.is_some() {
|
||||
assigned_port = Some(available_ports.alloc()?);
|
||||
}
|
||||
if options.add_ssl.is_some() {
|
||||
assigned_ssl_port = Some(available_ports.alloc()?);
|
||||
}
|
||||
if let Some(secure) = options.secure {
|
||||
if !secure.ssl || !options.add_ssl.is_some() {
|
||||
assigned_port = Some(available_ports.alloc()?);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
enabled: true,
|
||||
options,
|
||||
|
||||
@@ -176,29 +176,44 @@ impl Handler<RunAction> for ServiceActor {
|
||||
)
|
||||
.await
|
||||
.with_kind(ErrorKind::Action)?;
|
||||
if self
|
||||
let package_id = package_id.clone();
|
||||
for to_stop in self
|
||||
.0
|
||||
.ctx
|
||||
.db
|
||||
.mutate(|db| {
|
||||
let mut critical_activated = false;
|
||||
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
|
||||
critical_activated |= pde.as_tasks_mut().mutate(|tasks| {
|
||||
Ok(update_tasks(tasks, package_id, action_id, &input, true))
|
||||
})?;
|
||||
let mut to_stop = Vec::new();
|
||||
for (id, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
|
||||
if pde.as_tasks_mut().mutate(|tasks| {
|
||||
Ok(update_tasks(tasks, &package_id, action_id, &input, true))
|
||||
})? {
|
||||
to_stop.push(id)
|
||||
}
|
||||
}
|
||||
Ok(critical_activated)
|
||||
Ok(to_stop)
|
||||
})
|
||||
.await
|
||||
.result?
|
||||
{
|
||||
<Self as Handler<super::control::Stop>>::handle(
|
||||
self,
|
||||
id,
|
||||
super::control::Stop { wait: false },
|
||||
jobs,
|
||||
)
|
||||
.await;
|
||||
if to_stop == package_id {
|
||||
<Self as Handler<super::control::Stop>>::handle(
|
||||
self,
|
||||
id.clone(),
|
||||
super::control::Stop { wait: false },
|
||||
jobs,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
self.0
|
||||
.ctx
|
||||
.services
|
||||
.get(&to_stop)
|
||||
.await
|
||||
.as_ref()
|
||||
.or_not_found(&to_stop)?
|
||||
.stop(id.clone(), false)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -17,9 +17,6 @@ fi
|
||||
STARTOS_ENV="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/ENVIRONMENT.txt)"
|
||||
PLATFORM="$(dpkg-deb --fsys-tarfile $DEB_PATH | tar --to-stdout -xvf - ./usr/lib/startos/PLATFORM.txt)"
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
PLATFORM="$(uname -m)"
|
||||
fi
|
||||
if [ "$PLATFORM" = "x86_64" ] || [ "$PLATFORM" = "x86_64-nonfree" ]; then
|
||||
ARCH=amd64
|
||||
QEMU_ARCH=x86_64
|
||||
|
||||
@@ -104,7 +104,11 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
this.cachedParser = built.validator
|
||||
return {
|
||||
spec: built.spec,
|
||||
value: (await this.getInputFn(options)) || null,
|
||||
value:
|
||||
((await this.getInputFn(options)) as
|
||||
| Record<string, unknown>
|
||||
| null
|
||||
| undefined) || null,
|
||||
}
|
||||
}
|
||||
async run(options: {
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { ExtendedVersion, VersionRange } from "../exver"
|
||||
import { PackageId, HealthCheckId } from "../types"
|
||||
import {
|
||||
PackageId,
|
||||
HealthCheckId,
|
||||
DependencyRequirement,
|
||||
CheckDependenciesResult,
|
||||
} from "../types"
|
||||
import { Effects } from "../Effects"
|
||||
|
||||
export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
|
||||
infoFor: (packageId: DependencyId) => {
|
||||
requirement: DependencyRequirement
|
||||
result: CheckDependenciesResult
|
||||
}
|
||||
|
||||
installedSatisfied: (packageId: DependencyId) => boolean
|
||||
installedVersionSatisfied: (packageId: DependencyId) => boolean
|
||||
runningSatisfied: (packageId: DependencyId) => boolean
|
||||
@@ -41,7 +51,7 @@ export async function checkDependencies<
|
||||
)
|
||||
}
|
||||
|
||||
const find = (packageId: DependencyId) => {
|
||||
const infoFor = (packageId: DependencyId) => {
|
||||
const dependencyRequirement = dependencies.find((d) => d.id === packageId)
|
||||
const dependencyResult = results.find((d) => d.packageId === packageId)
|
||||
if (!dependencyRequirement || !dependencyResult) {
|
||||
@@ -51,9 +61,9 @@ export async function checkDependencies<
|
||||
}
|
||||
|
||||
const installedSatisfied = (packageId: DependencyId) =>
|
||||
!!find(packageId).result.installedVersion
|
||||
!!infoFor(packageId).result.installedVersion
|
||||
const installedVersionSatisfied = (packageId: DependencyId) => {
|
||||
const dep = find(packageId)
|
||||
const dep = infoFor(packageId)
|
||||
return (
|
||||
!!dep.result.installedVersion &&
|
||||
ExtendedVersion.parse(dep.result.installedVersion).satisfies(
|
||||
@@ -62,18 +72,18 @@ export async function checkDependencies<
|
||||
)
|
||||
}
|
||||
const runningSatisfied = (packageId: DependencyId) => {
|
||||
const dep = find(packageId)
|
||||
const dep = infoFor(packageId)
|
||||
return dep.requirement.kind !== "running" || dep.result.isRunning
|
||||
}
|
||||
const tasksSatisfied = (packageId: DependencyId) =>
|
||||
Object.entries(find(packageId).result.tasks).filter(
|
||||
Object.entries(infoFor(packageId).result.tasks).filter(
|
||||
([_, t]) => t.active && t.task.severity === "critical",
|
||||
).length === 0
|
||||
const healthCheckSatisfied = (
|
||||
packageId: DependencyId,
|
||||
healthCheckId?: HealthCheckId,
|
||||
) => {
|
||||
const dep = find(packageId)
|
||||
const dep = infoFor(packageId)
|
||||
if (
|
||||
healthCheckId &&
|
||||
(dep.requirement.kind !== "running" ||
|
||||
@@ -102,14 +112,14 @@ export async function checkDependencies<
|
||||
: dependencies.every((d) => pkgSatisfied(d.id as DependencyId))
|
||||
|
||||
const throwIfInstalledNotSatisfied = (packageId: DependencyId) => {
|
||||
const dep = find(packageId)
|
||||
const dep = infoFor(packageId)
|
||||
if (!dep.result.installedVersion) {
|
||||
throw new Error(`${dep.result.title || packageId} is not installed`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
const throwIfInstalledVersionNotSatisfied = (packageId: DependencyId) => {
|
||||
const dep = find(packageId)
|
||||
const dep = infoFor(packageId)
|
||||
if (!dep.result.installedVersion) {
|
||||
throw new Error(`${dep.result.title || packageId} is not installed`)
|
||||
}
|
||||
@@ -127,14 +137,14 @@ export async function checkDependencies<
|
||||
return null
|
||||
}
|
||||
const throwIfRunningNotSatisfied = (packageId: DependencyId) => {
|
||||
const dep = find(packageId)
|
||||
const dep = infoFor(packageId)
|
||||
if (dep.requirement.kind === "running" && !dep.result.isRunning) {
|
||||
throw new Error(`${dep.result.title || packageId} is not running`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
const throwIfTasksNotSatisfied = (packageId: DependencyId) => {
|
||||
const dep = find(packageId)
|
||||
const dep = infoFor(packageId)
|
||||
const reqs = Object.entries(dep.result.tasks)
|
||||
.filter(([_, t]) => t.active && t.task.severity === "critical")
|
||||
.map(([id, _]) => id)
|
||||
@@ -149,7 +159,7 @@ export async function checkDependencies<
|
||||
packageId: DependencyId,
|
||||
healthCheckId?: HealthCheckId,
|
||||
) => {
|
||||
const dep = find(packageId)
|
||||
const dep = infoFor(packageId)
|
||||
if (
|
||||
healthCheckId &&
|
||||
(dep.requirement.kind !== "running" ||
|
||||
@@ -205,6 +215,7 @@ export async function checkDependencies<
|
||||
})()
|
||||
|
||||
return {
|
||||
infoFor,
|
||||
installedSatisfied,
|
||||
installedVersionSatisfied,
|
||||
runningSatisfied,
|
||||
|
||||
@@ -753,7 +753,10 @@ export class Version {
|
||||
return "less"
|
||||
}
|
||||
|
||||
const prereleaseLen = Math.max(this.number.length, other.number.length)
|
||||
const prereleaseLen = Math.max(
|
||||
this.prerelease.length,
|
||||
other.prerelease.length,
|
||||
)
|
||||
for (let i = 0; i < prereleaseLen; i++) {
|
||||
if (typeof this.prerelease[i] === typeof other.prerelease[i]) {
|
||||
if (this.prerelease[i] > other.prerelease[i]) {
|
||||
|
||||
@@ -180,8 +180,8 @@ export type KnownError =
|
||||
|
||||
export type Dependencies = Array<DependencyRequirement>
|
||||
|
||||
export type DeepPartial<T> = T extends unknown[]
|
||||
? T
|
||||
export type DeepPartial<T> = T extends [infer A, ...infer Rest]
|
||||
? [DeepPartial<A>, ...DeepPartial<Rest>]
|
||||
: T extends {}
|
||||
? { [P in keyof T]?: DeepPartial<T[P]> }
|
||||
: T
|
||||
|
||||
@@ -26,7 +26,11 @@ export function partialDiff<T>(
|
||||
} else if (typeof prev === "object" && typeof next === "object") {
|
||||
if (prev === null || next === null) return { diff: next }
|
||||
const res = { diff: {} as Record<keyof T, any> }
|
||||
for (let key in next) {
|
||||
const keys = Object.keys(next) as (keyof T)[]
|
||||
for (let key in prev) {
|
||||
if (!keys.includes(key)) keys.push(key)
|
||||
}
|
||||
for (let key of keys) {
|
||||
const diff = partialDiff(prev[key], next[key])
|
||||
if (diff) {
|
||||
res.diff[key] = diff.diff
|
||||
|
||||
@@ -710,7 +710,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
image,
|
||||
mounts,
|
||||
name,
|
||||
)
|
||||
).then((subc) => subc.rc())
|
||||
},
|
||||
/**
|
||||
* @description Run a function with a temporary SubContainer
|
||||
|
||||
4
sdk/package/package-lock.json
generated
4
sdk/package/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.32",
|
||||
"version": "0.4.0-beta.33",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.32",
|
||||
"version": "0.4.0-beta.33",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.32",
|
||||
"version": "0.4.0-beta.33",
|
||||
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||
"main": "./package/lib/index.js",
|
||||
"types": "./package/lib/index.d.ts",
|
||||
|
||||
@@ -46,6 +46,7 @@ import { ClientStorageService } from './services/client-storage.service'
|
||||
import { DateTransformerService } from './services/date-transformer.service'
|
||||
import { DatetimeTransformerService } from './services/datetime-transformer.service'
|
||||
import { StorageService } from './services/storage.service'
|
||||
import { FilterUpdatesPipe } from './routes/portal/routes/updates/filter-updates.pipe'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
@@ -56,6 +57,7 @@ export const APP_PROVIDERS = [
|
||||
provideEventPlugins(),
|
||||
I18N_PROVIDERS,
|
||||
FilterPackagesPipe,
|
||||
FilterUpdatesPipe,
|
||||
UntypedFormBuilder,
|
||||
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
|
||||
tuiButtonOptionsProvider({ size: 'm' }),
|
||||
|
||||
@@ -103,7 +103,6 @@ import { HeaderStatusComponent } from './status.component'
|
||||
|
||||
&:has([data-status='neutral']) {
|
||||
--status: var(--tui-status-neutral);
|
||||
filter: none;
|
||||
}
|
||||
|
||||
&:has([data-status='success']) {
|
||||
|
||||
@@ -18,18 +18,18 @@
|
||||
|
||||
@if (followLogs | logs | async; as logs) {
|
||||
<section childList (waMutationObserver)="scrollToBottom()">
|
||||
@for (log of logs; track log) {
|
||||
@for (log of logs; track $index) {
|
||||
<pre [innerHTML]="log | dompurify"></pre>
|
||||
}
|
||||
|
||||
@if ((status$ | async) !== 'connected') {
|
||||
<p class="loading-dots" [attr.data-status]="status$.value">
|
||||
<div class="loading-dots" [attr.data-status]="status$.value">
|
||||
{{
|
||||
status$.value === 'reconnecting'
|
||||
? ('Reconnecting' | i18n)
|
||||
: ('Waiting for network connectivity' | i18n)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
} @else {
|
||||
|
||||
@@ -48,4 +48,5 @@
|
||||
pre {
|
||||
overflow: visible;
|
||||
white-space: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -8,16 +8,19 @@ import {
|
||||
import {
|
||||
bufferTime,
|
||||
catchError,
|
||||
concat,
|
||||
defer,
|
||||
delay,
|
||||
EMPTY,
|
||||
filter,
|
||||
ignoreElements,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
of,
|
||||
repeat,
|
||||
scan,
|
||||
skipWhile,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
@@ -62,12 +65,19 @@ export class LogsPipe implements PipeTransform {
|
||||
),
|
||||
).pipe(
|
||||
catchError(() =>
|
||||
this.connection.pipe(
|
||||
tap(v => this.logs.status$.next(v ? 'reconnecting' : 'disconnected')),
|
||||
filter(Boolean),
|
||||
take(1),
|
||||
ignoreElements(),
|
||||
startWith(this.getMessage(false)),
|
||||
concat(
|
||||
this.logs.status$.value === 'connected'
|
||||
? of(this.getMessage(false))
|
||||
: EMPTY,
|
||||
this.connection.pipe(
|
||||
tap(v =>
|
||||
this.logs.status$.next(v ? 'reconnecting' : 'disconnected'),
|
||||
),
|
||||
filter(Boolean),
|
||||
delay(1000),
|
||||
take(1),
|
||||
ignoreElements(),
|
||||
),
|
||||
),
|
||||
),
|
||||
repeat(),
|
||||
@@ -76,11 +86,11 @@ export class LogsPipe implements PipeTransform {
|
||||
}
|
||||
|
||||
private getMessage(success: boolean): string {
|
||||
return `<p style="color: ${
|
||||
return `<div style="color: ${
|
||||
success ? 'var(--tui-status-positive)' : 'var(--tui-status-negative)'
|
||||
}; text-align: center;">${this.i18n.transform(
|
||||
success ? 'Reconnected' : 'Disconnected',
|
||||
)} at ${toLocalIsoString(new Date())}</p>`
|
||||
)} at ${toLocalIsoString(new Date())}</div>`
|
||||
}
|
||||
|
||||
private get options() {
|
||||
|
||||
@@ -26,7 +26,7 @@ import { HeaderComponent } from './components/header/header.component'
|
||||
</main>
|
||||
<app-tabs />
|
||||
@if (update(); as update) {
|
||||
<tui-action-bar *tuiActionBar="bar">
|
||||
<tui-action-bar *tuiActionBar="bar()">
|
||||
@if (update === true) {
|
||||
<tui-icon icon="@tui.check" class="g-positive" />
|
||||
Download complete, restart to apply changes
|
||||
@@ -77,8 +77,7 @@ import { HeaderComponent } from './components/header/header.component'
|
||||
|
||||
@include taiga.transition(filter);
|
||||
|
||||
header:has([data-status='success']) + &,
|
||||
header:has([data-status='neutral']) + & {
|
||||
header:has([data-status='success']) + & {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
@@ -104,7 +103,7 @@ export class PortalComponent {
|
||||
|
||||
readonly name = toSignal(this.patch.watch$('ui', 'name'))
|
||||
readonly update = toSignal(inject(OSService).updating$)
|
||||
bar = true
|
||||
readonly bar = signal(true)
|
||||
|
||||
getProgress(size: number, downloaded: number): number {
|
||||
return Math.round((100 * downloaded) / (size || 1))
|
||||
@@ -114,7 +113,7 @@ export class PortalComponent {
|
||||
const loader = this.loader.open('Beginning restart').subscribe()
|
||||
|
||||
try {
|
||||
this.bar = false
|
||||
this.bar.set(false)
|
||||
await this.api.restartServer({})
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
|
||||
@@ -3,11 +3,9 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { Router } from '@angular/router'
|
||||
import { MarketplacePkgBase } from '@start9labs/marketplace'
|
||||
import {
|
||||
ErrorService,
|
||||
Exver,
|
||||
@@ -30,21 +28,20 @@ import {
|
||||
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'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-controls',
|
||||
template: `
|
||||
@if (localPkg) {
|
||||
@if (localPkg | toManifest; as localManifest) {
|
||||
@switch (localManifest.version | compareExver: version() || '') {
|
||||
@if (localPkg(); as local) {
|
||||
@if (local.stateInfo.state === 'installed') {
|
||||
@switch ((local | toManifest).version | compareExver: version()) {
|
||||
@case (1) {
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="secondary-destructive"
|
||||
appearance="warning"
|
||||
(click)="tryInstall()"
|
||||
>
|
||||
{{ 'Downgrade' | i18n }}
|
||||
@@ -81,17 +78,17 @@ import { MarketplaceAlertsService } from '../services/alerts.service'
|
||||
{{
|
||||
('View' | i18n) +
|
||||
' ' +
|
||||
($any(localPkg.stateInfo.state | titlecase) | i18n)
|
||||
($any(local.stateInfo.state | titlecase) | i18n)
|
||||
}}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
[appearance]="localFlavor ? 'warning' : 'primary'"
|
||||
appearance="primary"
|
||||
(click)="tryInstall()"
|
||||
>
|
||||
{{ localFlavor ? ('Switch' | i18n) : ('Install' | i18n) }}
|
||||
{{ localFlavor() ? ('Switch' | i18n) : ('Install' | i18n) }}
|
||||
</button>
|
||||
}
|
||||
`,
|
||||
@@ -116,29 +113,23 @@ export class MarketplaceControlsComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly preview = inject(MarketplacePreviewComponent)
|
||||
|
||||
protected readonly version = toSignal(this.preview.version$)
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkgBase
|
||||
|
||||
@Input()
|
||||
localPkg!: PackageDataEntry | null
|
||||
|
||||
@Input()
|
||||
localFlavor!: boolean
|
||||
|
||||
version = input.required<string>()
|
||||
installAlert = input.required<string | null>()
|
||||
localPkg = input.required<PackageDataEntry | null>()
|
||||
localFlavor = input.required<boolean>()
|
||||
// only present if side loading
|
||||
@Input()
|
||||
file?: File
|
||||
file = input<File>()
|
||||
|
||||
async tryInstall() {
|
||||
const currentUrl = this.file
|
||||
const localPkg = this.localPkg()
|
||||
|
||||
const currentUrl = this.file()
|
||||
? null
|
||||
: await firstValueFrom(this.marketplaceService.currentRegistryUrl$)
|
||||
const originalUrl = this.localPkg?.registry || null
|
||||
const originalUrl = localPkg?.registry || null
|
||||
|
||||
if (!this.localPkg) {
|
||||
if (await this.alerts.alertInstall(this.pkg)) {
|
||||
if (!localPkg) {
|
||||
if (await this.alerts.alertInstall(this.installAlert() || '')) {
|
||||
this.installOrUpload(currentUrl)
|
||||
}
|
||||
return
|
||||
@@ -152,12 +143,11 @@ export class MarketplaceControlsComponent {
|
||||
return
|
||||
}
|
||||
|
||||
const localManifest = getManifest(this.localPkg)
|
||||
const version = this.version() || ''
|
||||
const localManifest = getManifest(localPkg)
|
||||
|
||||
if (
|
||||
hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) &&
|
||||
this.exver.compareExver(localManifest.version, version) !== 0
|
||||
this.exver.compareExver(localManifest.version, this.version()) !== 0
|
||||
) {
|
||||
this.dryInstall(currentUrl)
|
||||
} else {
|
||||
@@ -171,9 +161,8 @@ export class MarketplaceControlsComponent {
|
||||
|
||||
private async dryInstall(url: string | null) {
|
||||
const id = this.preview.pkgId
|
||||
const version = this.version() || ''
|
||||
const breakages = dryUpdate(
|
||||
{ id, version },
|
||||
{ id, version: this.version() },
|
||||
await getAllPackages(this.patch),
|
||||
this.exver,
|
||||
)
|
||||
@@ -187,7 +176,7 @@ export class MarketplaceControlsComponent {
|
||||
}
|
||||
|
||||
private async installOrUpload(url: string | null) {
|
||||
if (this.file) {
|
||||
if (this.file()) {
|
||||
await this.upload()
|
||||
this.router.navigate(['/portal', 'services'])
|
||||
} else if (url) {
|
||||
@@ -197,11 +186,10 @@ export class MarketplaceControlsComponent {
|
||||
|
||||
private async install(url: string) {
|
||||
const loader = this.loader.open('Beginning install').subscribe()
|
||||
const version = this.version() || ''
|
||||
const id = this.preview.pkgId
|
||||
|
||||
try {
|
||||
await this.marketplaceService.installPackage(id, version, url)
|
||||
await this.marketplaceService.installPackage(id, this.version(), url)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
@@ -210,11 +198,14 @@ export class MarketplaceControlsComponent {
|
||||
}
|
||||
|
||||
private async upload() {
|
||||
const file = this.file()
|
||||
if (!file) throw new Error('no file detected')
|
||||
|
||||
const loader = this.loader.open('Starting upload').subscribe()
|
||||
|
||||
try {
|
||||
const { upload } = await this.api.sideloadPackage()
|
||||
this.api.uploadPackage(upload, this.file!).catch(console.error)
|
||||
this.api.uploadPackage(upload, file).catch(console.error)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
|
||||
@@ -56,7 +56,8 @@ import { MarketplaceControlsComponent } from './controls.component'
|
||||
<marketplace-controls
|
||||
slot="controls"
|
||||
class="controls-wrapper"
|
||||
[pkg]="pkg()"
|
||||
[version]="pkg().version"
|
||||
[installAlert]="pkg().alerts.install"
|
||||
[localPkg]="local$ | async"
|
||||
[localFlavor]="!!(flavor$ | async)"
|
||||
/>
|
||||
|
||||
@@ -62,9 +62,7 @@ export class MarketplaceAlertsService {
|
||||
})
|
||||
}
|
||||
|
||||
async alertInstall({ alerts }: MarketplacePkgBase): Promise<boolean> {
|
||||
const content = alerts.install
|
||||
|
||||
async alertInstall(content: string): Promise<boolean> {
|
||||
return (
|
||||
!content ||
|
||||
(!!content &&
|
||||
|
||||
@@ -90,17 +90,20 @@ export type PackageActionData = {
|
||||
`,
|
||||
styles: `
|
||||
tui-notification {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1.4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.service-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
img {
|
||||
height: 20px;
|
||||
margin-right: 4px;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.25rem;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -192,7 +195,7 @@ export class ActionInputModal {
|
||||
task.when?.condition === 'input-not-matches' &&
|
||||
task.input &&
|
||||
json
|
||||
.compare(input, task.input)
|
||||
.compare(input, task.input.value)
|
||||
.some(op => op.op === 'add' || op.op === 'replace'),
|
||||
),
|
||||
)
|
||||
@@ -201,9 +204,8 @@ export class ActionInputModal {
|
||||
if (!breakages.length) return true
|
||||
|
||||
const message = `${this.i18n.transform('As a result of this change, the following services will no longer work properly and may crash')}:<ul>`
|
||||
const content = `${message}${breakages.map(
|
||||
id => `<li><b>${getManifest(packages[id]!).title}</b></li>`,
|
||||
)}</ul>` as i18nKey
|
||||
const content =
|
||||
`${message}${breakages.map(id => `<li><b>${getManifest(packages[id]!).title}</b></li>`)}</ul>` as i18nKey
|
||||
|
||||
return firstValueFrom(
|
||||
this.dialog
|
||||
|
||||
@@ -29,7 +29,8 @@ import { MarketplacePkgSideload } from './sideload.utils'
|
||||
<marketplace-controls
|
||||
slot="controls"
|
||||
class="controls-wrapper"
|
||||
[pkg]="pkg()"
|
||||
[version]="pkg().version"
|
||||
[installAlert]="pkg().alerts.install"
|
||||
[localPkg]="local$ | async"
|
||||
[localFlavor]="!!(flavor$ | async)"
|
||||
[file]="file()"
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||
import { Exver } from '@start9labs/shared'
|
||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||
import {
|
||||
InstalledState,
|
||||
PackageDataEntry,
|
||||
UpdatingState,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Pipe({
|
||||
name: 'filterUpdates',
|
||||
@@ -15,15 +11,14 @@ export class FilterUpdatesPipe implements PipeTransform {
|
||||
|
||||
transform(
|
||||
pkgs: MarketplacePkg[],
|
||||
local: Record<
|
||||
string,
|
||||
PackageDataEntry<InstalledState | UpdatingState>
|
||||
> = {},
|
||||
local: Record<string, PackageDataEntry> = {},
|
||||
): MarketplacePkg[] {
|
||||
return pkgs.filter(({ id, version, flavor }) => {
|
||||
return pkgs.filter(({ id, flavor, version }) => {
|
||||
const localPkg = local[id]
|
||||
return (
|
||||
localPkg &&
|
||||
!!localPkg &&
|
||||
(localPkg.stateInfo.state === 'installed' ||
|
||||
localPkg.stateInfo.state === 'updating') &&
|
||||
this.exver.getFlavor(localPkg.stateInfo.manifest.version) === flavor &&
|
||||
this.exver.compareExver(
|
||||
version,
|
||||
|
||||
@@ -44,7 +44,7 @@ import UpdatesComponent from './updates.component'
|
||||
template: `
|
||||
<tr (click)="expanded.set(!expanded())">
|
||||
<td>
|
||||
<div [style.gap.rem]="0.75">
|
||||
<div [style.gap.rem]="0.75" [style.padding-inline-end.rem]="1">
|
||||
<tui-avatar size="s"><img alt="" [src]="item().icon" /></tui-avatar>
|
||||
<span tuiTitle [style.margin]="'-0.125rem 0 0'">
|
||||
<b tuiFade>{{ item().title }}</b>
|
||||
@@ -81,7 +81,6 @@ import UpdatesComponent from './updates.component'
|
||||
</button>
|
||||
@if (local().stateInfo.state === 'updating') {
|
||||
<tui-progress-circle
|
||||
class="g-positive"
|
||||
size="xs"
|
||||
[max]="100"
|
||||
[value]="
|
||||
@@ -175,7 +174,7 @@ import UpdatesComponent from './updates.component'
|
||||
white-space: nowrap;
|
||||
|
||||
div {
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { Exver } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
combineLatest,
|
||||
@@ -19,13 +18,13 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { NotificationService } from 'src/app/services/notification.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { FilterUpdatesPipe } from '../routes/portal/routes/updates/filter-updates.pipe'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class BadgeService {
|
||||
private readonly notifications = inject(NotificationService)
|
||||
private readonly exver = inject(Exver)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly system$ = inject(OSService).updateAvailable$.pipe(
|
||||
map(Number),
|
||||
@@ -34,6 +33,7 @@ export class BadgeService {
|
||||
.watch$('serverInfo', 'ntpSynced')
|
||||
.pipe(map(synced => Number(!synced)))
|
||||
private readonly marketplaceService = inject(MarketplaceService)
|
||||
private readonly filterUpdatesPipe = inject(FilterUpdatesPipe)
|
||||
|
||||
private readonly local$ = inject(ConnectionService).pipe(
|
||||
filter(Boolean),
|
||||
@@ -66,17 +66,9 @@ export class BadgeService {
|
||||
([marketplace, local]) =>
|
||||
Object.entries(marketplace).reduce(
|
||||
(list, [_, store]) =>
|
||||
store?.packages.reduce(
|
||||
(result, { id, version }) =>
|
||||
local[id] &&
|
||||
this.exver.compareExver(
|
||||
version,
|
||||
getManifest(local[id]!).version,
|
||||
) === 1
|
||||
? result.add(id)
|
||||
: result,
|
||||
list,
|
||||
) || list,
|
||||
this.filterUpdatesPipe
|
||||
.transform(store?.packages || [], local)
|
||||
.reduce((result, { id }) => result.add(id), list),
|
||||
new Set<string>(),
|
||||
).size,
|
||||
),
|
||||
|
||||
@@ -57,6 +57,10 @@ export class StandardActionsService {
|
||||
content = `${content}${content ? ' ' : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return this.doUninstall({ id, force, soft })
|
||||
}
|
||||
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
label: 'Warning',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { CanActivateFn, IsActiveMatchOptions, Router } from '@angular/router'
|
||||
import { DialogService, i18nPipe } from '@start9labs/shared'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { TUI_TRUE_HANDLER } from '@taiga-ui/cdk'
|
||||
import { TuiAlertService } from '@taiga-ui/core'
|
||||
import {
|
||||
@@ -47,9 +47,7 @@ export class StateService extends Observable<RR.ServerState | null> {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly router = inject(Router)
|
||||
private readonly network$ = inject(NetworkService)
|
||||
|
||||
private readonly single$ = new Subject<RR.ServerState>()
|
||||
|
||||
private readonly trigger$ = new BehaviorSubject<void>(undefined)
|
||||
private readonly poll$ = this.trigger$.pipe(
|
||||
switchMap(() =>
|
||||
@@ -101,7 +99,7 @@ export class StateService extends Observable<RR.ServerState | null> {
|
||||
})
|
||||
.pipe(
|
||||
takeUntil(
|
||||
combineLatest([this.stream$, this.network$]).pipe(
|
||||
combineLatest([this.stream$.pipe(skip(1)), this.network$]).pipe(
|
||||
filter(state => state.every(Boolean)),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
{
|
||||
"/rpc/v1": {
|
||||
"target": "http://<CHANGE_ME>/rpc/v1"
|
||||
"target": "http://<CHANGE_ME>"
|
||||
},
|
||||
"/ws/*": {
|
||||
"target": "http://<CHANGE_ME>",
|
||||
"/ws/**": {
|
||||
"target": "wss://159.223.45.210",
|
||||
"secure": false,
|
||||
"ws": true
|
||||
},
|
||||
"/public/*": {
|
||||
"target": "http://<CHANGE_ME>/public",
|
||||
"pathRewrite": {
|
||||
"^/public": ""
|
||||
}
|
||||
"/public/**": {
|
||||
"target": "http://<CHANGE_ME>"
|
||||
},
|
||||
"/rest/rpc/*": {
|
||||
"target": "http://<CHANGE_ME>/rest/rpc",
|
||||
"pathRewrite": {
|
||||
"^/rest/rpc": ""
|
||||
}
|
||||
"/rest/rpc/**": {
|
||||
"target": "http://<CHANGE_ME>"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user