mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
Feature/shared refactor (#1176)
* refactor: move most of the shared entities to @start8labs/shared library
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "shared",
|
||||
"name": "@start9labs/shared",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^13.2.0",
|
||||
"@angular/core": "^13.2.0"
|
||||
"@angular/core": "^13.2.0",
|
||||
"@start9labs/emver": "^0.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<ion-grid class="full-height">
|
||||
<ion-row class="ion-align-items-center ion-text-center full-height">
|
||||
<ion-col>
|
||||
<ion-spinner name="lines" color="warning"></ion-spinner>
|
||||
<p>{{ text }}</p>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { TextSpinnerComponent } from './text-spinner.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [TextSpinnerComponent],
|
||||
imports: [CommonModule, IonicModule],
|
||||
exports: [TextSpinnerComponent],
|
||||
})
|
||||
export class TextSpinnerComponentModule {}
|
||||
@@ -0,0 +1,3 @@
|
||||
.full-height {
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'text-spinner',
|
||||
templateUrl: './text-spinner.component.html',
|
||||
styleUrls: ['./text-spinner.component.scss'],
|
||||
})
|
||||
export class TextSpinnerComponent {
|
||||
@Input() text = ''
|
||||
}
|
||||
12
frontend/projects/shared/src/pipes/emver/emver.module.ts
Normal file
12
frontend/projects/shared/src/pipes/emver/emver.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import {
|
||||
EmverComparesPipe,
|
||||
EmverDisplayPipe,
|
||||
EmverSatisfiesPipe,
|
||||
} from './emver.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [EmverComparesPipe, EmverDisplayPipe, EmverSatisfiesPipe],
|
||||
exports: [EmverComparesPipe, EmverDisplayPipe, EmverSatisfiesPipe],
|
||||
})
|
||||
export class EmverPipesModule {}
|
||||
47
frontend/projects/shared/src/pipes/emver/emver.pipe.ts
Normal file
47
frontend/projects/shared/src/pipes/emver/emver.pipe.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { Emver } from '../../services/emver.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'satisfiesEmver',
|
||||
})
|
||||
export class EmverSatisfiesPipe implements PipeTransform {
|
||||
constructor(private readonly emver: Emver) {}
|
||||
|
||||
transform(versionUnderTest: string, range: string): boolean {
|
||||
return this.emver.satisfies(versionUnderTest, range)
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'compareEmver',
|
||||
})
|
||||
export class EmverComparesPipe implements PipeTransform {
|
||||
constructor(private readonly emver: Emver) {}
|
||||
|
||||
transform(first: string, second: string): SemverResult {
|
||||
try {
|
||||
return this.emver.compare(first, second) as SemverResult
|
||||
} catch (e) {
|
||||
console.warn(`emver comparison failed`, e, first, second)
|
||||
return 'comparison-impossible'
|
||||
}
|
||||
}
|
||||
}
|
||||
type SemverResult = 0 | 1 | -1 | 'comparison-impossible'
|
||||
|
||||
@Pipe({
|
||||
name: 'displayEmver',
|
||||
})
|
||||
export class EmverDisplayPipe implements PipeTransform {
|
||||
constructor() {}
|
||||
|
||||
transform(version: string): string {
|
||||
return displayEmver(version)
|
||||
}
|
||||
}
|
||||
|
||||
export function displayEmver(version: string): string {
|
||||
const vs = version.split('.')
|
||||
if (vs.length === 4) return `${vs[0]}.${vs[1]}.${vs[2]}~${vs[3]}`
|
||||
return version
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { MarkdownPipe } from './markdown.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [MarkdownPipe],
|
||||
exports: [MarkdownPipe],
|
||||
})
|
||||
export class MarkdownPipeModule {}
|
||||
17
frontend/projects/shared/src/pipes/markdown/markdown.pipe.ts
Normal file
17
frontend/projects/shared/src/pipes/markdown/markdown.pipe.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { marked } from 'marked'
|
||||
import * as DOMPurify from 'dompurify'
|
||||
|
||||
@Pipe({
|
||||
name: 'markdown',
|
||||
})
|
||||
export class MarkdownPipe implements PipeTransform {
|
||||
transform(value: any): any {
|
||||
if (value && value.length > 0) {
|
||||
const html = marked(value)
|
||||
const sanitized = DOMPurify.sanitize(html)
|
||||
return sanitized
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
12
frontend/projects/shared/src/pipes/shared/empty.pipe.ts
Normal file
12
frontend/projects/shared/src/pipes/shared/empty.pipe.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { isEmptyObject } from '../../util/misc.util'
|
||||
|
||||
@Pipe({
|
||||
name: 'empty',
|
||||
})
|
||||
export class EmptyPipe implements PipeTransform {
|
||||
transform(val: object | [] = {}): boolean {
|
||||
if (Array.isArray(val)) return !val.length
|
||||
return isEmptyObject(val)
|
||||
}
|
||||
}
|
||||
10
frontend/projects/shared/src/pipes/shared/includes.pipe.ts
Normal file
10
frontend/projects/shared/src/pipes/shared/includes.pipe.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
|
||||
@Pipe({
|
||||
name: 'includes',
|
||||
})
|
||||
export class IncludesPipe implements PipeTransform {
|
||||
transform<T>(list: T[], val: T): boolean {
|
||||
return list.includes(val)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { IncludesPipe } from './includes.pipe'
|
||||
import { EmptyPipe } from './empty.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [IncludesPipe, EmptyPipe],
|
||||
exports: [IncludesPipe, EmptyPipe],
|
||||
})
|
||||
export class SharedPipesModule {}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { ConvertBytesPipe, DurationToSecondsPipe } from './unit-conversion.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ConvertBytesPipe, DurationToSecondsPipe],
|
||||
exports: [ConvertBytesPipe, DurationToSecondsPipe],
|
||||
})
|
||||
export class UnitConversionPipesModule {}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
|
||||
// converts bytes to gigabytes
|
||||
@Pipe({
|
||||
name: 'convertBytes',
|
||||
})
|
||||
export class ConvertBytesPipe implements PipeTransform {
|
||||
transform(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
|
||||
const k = 1024
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'durationToSeconds',
|
||||
})
|
||||
export class DurationToSecondsPipe implements PipeTransform {
|
||||
transform(duration: string | null): number {
|
||||
if (!duration) return 0
|
||||
const splitUnit = duration.match(/^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/)
|
||||
const unit = splitUnit[3]
|
||||
const num = splitUnit[1]
|
||||
return Number(num) * unitsToSeconds[unit]
|
||||
}
|
||||
}
|
||||
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
|
||||
const unitsToSeconds = {
|
||||
ns: 1e-9,
|
||||
µs: 1e-6,
|
||||
ms: 0.001,
|
||||
s: 1,
|
||||
m: 60,
|
||||
h: 3600,
|
||||
d: 86400,
|
||||
}
|
||||
@@ -1,5 +1,28 @@
|
||||
/*
|
||||
* Public API Surface of shared
|
||||
* Public API Surface of @start9labs/shared
|
||||
*/
|
||||
|
||||
export * from './components/text-spinner/text-spinner.component.module'
|
||||
export * from './components/text-spinner/text-spinner.component'
|
||||
|
||||
export * from './pipes/emver/emver.module'
|
||||
export * from './pipes/emver/emver.pipe'
|
||||
export * from './pipes/markdown/markdown.module'
|
||||
export * from './pipes/markdown/markdown.pipe'
|
||||
export * from './pipes/shared/shared.module'
|
||||
export * from './pipes/shared/empty.pipe'
|
||||
export * from './pipes/shared/includes.pipe'
|
||||
export * from './pipes/unit-conversion/unit-conversion.module'
|
||||
export * from './pipes/unit-conversion/unit-conversion.pipe'
|
||||
|
||||
export * from './services/destroy.service'
|
||||
export * from './services/emver.service'
|
||||
|
||||
export * from './types/dependent-info'
|
||||
export * from './types/install-progress'
|
||||
export * from './types/package-state'
|
||||
export * from './types/progress-data'
|
||||
export * from './types/workspace-config'
|
||||
|
||||
export * from './util/misc.util'
|
||||
export * from './util/package-loading-progress'
|
||||
|
||||
13
frontend/projects/shared/src/services/destroy.service.ts
Normal file
13
frontend/projects/shared/src/services/destroy.service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable, OnDestroy } from '@angular/core'
|
||||
import { ReplaySubject } from 'rxjs'
|
||||
|
||||
/**
|
||||
* Observable abstraction over ngOnDestroy to use with takeUntil
|
||||
*/
|
||||
@Injectable()
|
||||
export class DestroyService extends ReplaySubject<void> implements OnDestroy {
|
||||
ngOnDestroy() {
|
||||
this.next()
|
||||
this.complete()
|
||||
}
|
||||
}
|
||||
18
frontend/projects/shared/src/services/emver.service.ts
Normal file
18
frontend/projects/shared/src/services/emver.service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import * as emver from '@start9labs/emver'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class Emver {
|
||||
constructor() {}
|
||||
|
||||
compare(lhs: string, rhs: string): number {
|
||||
if (!lhs || !rhs) return null
|
||||
return emver.compare(lhs, rhs)
|
||||
}
|
||||
|
||||
satisfies(version: string, range: string): boolean {
|
||||
return emver.satisfies(version, range)
|
||||
}
|
||||
}
|
||||
5
frontend/projects/shared/src/types/dependent-info.ts
Normal file
5
frontend/projects/shared/src/types/dependent-info.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface DependentInfo {
|
||||
id: string
|
||||
title: string
|
||||
version?: string
|
||||
}
|
||||
9
frontend/projects/shared/src/types/install-progress.ts
Normal file
9
frontend/projects/shared/src/types/install-progress.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface InstallProgress {
|
||||
size: number | null
|
||||
downloaded: number
|
||||
'download-complete': boolean
|
||||
validated: number
|
||||
'validation-complete': boolean
|
||||
unpacked: number
|
||||
'unpack-complete': boolean
|
||||
}
|
||||
7
frontend/projects/shared/src/types/package-state.ts
Normal file
7
frontend/projects/shared/src/types/package-state.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum PackageState {
|
||||
Installing = 'installing',
|
||||
Installed = 'installed',
|
||||
Updating = 'updating',
|
||||
Removing = 'removing',
|
||||
Restoring = 'restoring',
|
||||
}
|
||||
7
frontend/projects/shared/src/types/progress-data.ts
Normal file
7
frontend/projects/shared/src/types/progress-data.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface ProgressData {
|
||||
totalProgress: number
|
||||
downloadProgress: number
|
||||
validateProgress: number
|
||||
unpackProgress: number
|
||||
isComplete: boolean
|
||||
}
|
||||
165
frontend/projects/shared/src/util/misc.util.ts
Normal file
165
frontend/projects/shared/src/util/misc.util.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { OperatorFunction } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
|
||||
export function trace<T>(t: T): T {
|
||||
console.log(`TRACE`, t)
|
||||
return t
|
||||
}
|
||||
|
||||
// curried description. This allows e.g somePromise.thentraceDesc('my result'))
|
||||
export function traceDesc<T>(description: string): (t: T) => T {
|
||||
return t => {
|
||||
console.log(`TRACE`, description, t)
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
// for use in observables. This allows e.g. someObservable.pipe(traceM('my result'))
|
||||
// the practical equivalent of `tap(t => console.log(t, description))`
|
||||
export function traceWheel<T>(description?: string): OperatorFunction<T, T> {
|
||||
return description ? map(traceDesc(description)) : map(trace)
|
||||
}
|
||||
|
||||
export function traceThrowDesc<T>(description: string, t: T | undefined): T {
|
||||
if (!t) throw new Error(description)
|
||||
return t
|
||||
}
|
||||
|
||||
export function inMs(
|
||||
count: number,
|
||||
unit: 'days' | 'hours' | 'minutes' | 'seconds',
|
||||
) {
|
||||
switch (unit) {
|
||||
case 'seconds':
|
||||
return count * 1000
|
||||
case 'minutes':
|
||||
return inMs(count * 60, 'seconds')
|
||||
case 'hours':
|
||||
return inMs(count * 60, 'minutes')
|
||||
case 'days':
|
||||
return inMs(count * 24, 'hours')
|
||||
}
|
||||
}
|
||||
|
||||
// arr1 - arr2
|
||||
export function diff<T>(arr1: T[], arr2: T[]): T[] {
|
||||
return arr1.filter(x => !arr2.includes(x))
|
||||
}
|
||||
|
||||
// arr1 & arr2
|
||||
export function both<T>(arr1: T[], arr2: T[]): T[] {
|
||||
return arr1.filter(x => arr2.includes(x))
|
||||
}
|
||||
|
||||
export function isObject(val: any): boolean {
|
||||
return val && typeof val === 'object' && !Array.isArray(val)
|
||||
}
|
||||
|
||||
export function isEmptyObject(obj: object): boolean {
|
||||
if (obj === undefined) return true
|
||||
return !Object.keys(obj).length
|
||||
}
|
||||
|
||||
export function pauseFor(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
export function toObject<T>(t: T[], map: (t0: T) => string): Record<string, T> {
|
||||
return t.reduce((acc, next) => {
|
||||
acc[map(next)] = next
|
||||
return acc
|
||||
}, {} as Record<string, T>)
|
||||
}
|
||||
|
||||
export function update<T>(
|
||||
t: Record<string, T>,
|
||||
u: Record<string, T>,
|
||||
): Record<string, T> {
|
||||
return { ...t, ...u }
|
||||
}
|
||||
|
||||
export function deepCloneUnknown<T>(value: T): T {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return deepCloneArray(value)
|
||||
}
|
||||
return deepCloneObject(value)
|
||||
}
|
||||
|
||||
export function deepCloneObject<T>(source: T) {
|
||||
const result = {}
|
||||
Object.keys(source).forEach(key => {
|
||||
const value = source[key]
|
||||
result[key] = deepCloneUnknown(value)
|
||||
}, {})
|
||||
return result as T
|
||||
}
|
||||
|
||||
export function deepCloneArray(collection: any) {
|
||||
return collection.map(value => {
|
||||
return deepCloneUnknown(value)
|
||||
})
|
||||
}
|
||||
|
||||
export function partitionArray<T>(
|
||||
ts: T[],
|
||||
condition: (t: T) => boolean,
|
||||
): [T[], T[]] {
|
||||
const yes = [] as T[]
|
||||
const no = [] as T[]
|
||||
ts.forEach(t => {
|
||||
if (condition(t)) {
|
||||
yes.push(t)
|
||||
} else {
|
||||
no.push(t)
|
||||
}
|
||||
})
|
||||
return [yes, no]
|
||||
}
|
||||
|
||||
export function uniqueBy<T>(
|
||||
ts: T[],
|
||||
uniqueBy: (t: T) => string,
|
||||
prioritize: (t1: T, t2: T) => T,
|
||||
) {
|
||||
return Object.values(
|
||||
ts.reduce((acc, next) => {
|
||||
const previousValue = acc[uniqueBy(next)]
|
||||
if (previousValue) {
|
||||
acc[uniqueBy(next)] = prioritize(acc[uniqueBy(next)], previousValue)
|
||||
} else {
|
||||
acc[uniqueBy(next)] = previousValue
|
||||
}
|
||||
return acc
|
||||
}, {}),
|
||||
)
|
||||
}
|
||||
|
||||
export function capitalizeFirstLetter(string: string): string {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
||||
export const exists = (t: any) => {
|
||||
return t !== undefined
|
||||
}
|
||||
|
||||
export function debounce(delay: number = 300): MethodDecorator {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor,
|
||||
) {
|
||||
const timeoutKey = Symbol()
|
||||
|
||||
const original = descriptor.value
|
||||
|
||||
descriptor.value = function (...args) {
|
||||
clearTimeout(this[timeoutKey])
|
||||
this[timeoutKey] = setTimeout(() => original.apply(this, args), delay)
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { isEmptyObject } from './misc.util'
|
||||
import { InstallProgress } from '../types/install-progress'
|
||||
import { ProgressData } from '../types/progress-data'
|
||||
|
||||
export function packageLoadingProgress(
|
||||
loadData: InstallProgress,
|
||||
): ProgressData | null {
|
||||
if (isEmptyObject(loadData)) {
|
||||
return null
|
||||
}
|
||||
|
||||
let {
|
||||
downloaded,
|
||||
validated,
|
||||
unpacked,
|
||||
size,
|
||||
'download-complete': downloadComplete,
|
||||
'validation-complete': validationComplete,
|
||||
'unpack-complete': unpackComplete,
|
||||
} = loadData
|
||||
|
||||
// only permit 100% when "complete" == true
|
||||
downloaded = downloadComplete ? size : Math.max(downloaded - 1, 0)
|
||||
validated = validationComplete ? size : Math.max(validated - 1, 0)
|
||||
unpacked = unpackComplete ? size : Math.max(unpacked - 1, 0)
|
||||
|
||||
const downloadWeight = 1
|
||||
const validateWeight = 0.2
|
||||
const unpackWeight = 0.7
|
||||
|
||||
const numerator = Math.floor(
|
||||
downloadWeight * downloaded +
|
||||
validateWeight * validated +
|
||||
unpackWeight * unpacked,
|
||||
)
|
||||
|
||||
const denominator = Math.floor(
|
||||
size * (downloadWeight + validateWeight + unpackWeight),
|
||||
)
|
||||
const totalProgress = Math.floor((100 * numerator) / denominator)
|
||||
|
||||
return {
|
||||
totalProgress,
|
||||
downloadProgress: Math.floor((100 * downloaded) / size),
|
||||
validateProgress: Math.floor((100 * validated) / size),
|
||||
unpackProgress: Math.floor((100 * unpacked) / size),
|
||||
isComplete: downloadComplete && validationComplete && unpackComplete,
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "../../out-tsc/lib",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
Reference in New Issue
Block a user