Feature/shared refactor (#1176)

* refactor: move most of the shared entities to @start8labs/shared library
This commit is contained in:
Alex Inkin
2022-02-15 18:13:05 +03:00
committed by GitHub
parent 1769f7e2e6
commit 7c26b18c73
164 changed files with 2908 additions and 2860 deletions

View File

@@ -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"

View File

@@ -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>

View File

@@ -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 {}

View File

@@ -0,0 +1,3 @@
.full-height {
height: 100%;
}

View File

@@ -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 = ''
}

View 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 {}

View 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
}

View File

@@ -0,0 +1,8 @@
import { NgModule } from '@angular/core'
import { MarkdownPipe } from './markdown.pipe'
@NgModule({
declarations: [MarkdownPipe],
exports: [MarkdownPipe],
})
export class MarkdownPipeModule {}

View 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
}
}

View 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)
}
}

View 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)
}
}

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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,
}

View File

@@ -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'

View 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()
}
}

View 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)
}
}

View File

@@ -0,0 +1,5 @@
export interface DependentInfo {
id: string
title: string
version?: string
}

View 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
}

View File

@@ -0,0 +1,7 @@
export enum PackageState {
Installing = 'installing',
Installed = 'installed',
Updating = 'updating',
Removing = 'removing',
Restoring = 'restoring',
}

View File

@@ -0,0 +1,7 @@
export interface ProgressData {
totalProgress: number
downloadProgress: number
validateProgress: number
unpackProgress: number
isComplete: boolean
}

View 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
}
}

View File

@@ -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,
}
}

View File

@@ -2,6 +2,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "../../out-tsc/lib",
"declaration": true,
"declarationMap": true,