feat: as docker mirror registry

feat: add global proxy config
upgrade: upgrade front(angular) to 19
chore: deployment staff
  1. Dockerfile: build frontend, backend, and run in nginx base image
This commit is contained in:
loveuer
2024-12-23 00:07:44 -08:00
parent aac6c67a5f
commit 6e866b83e4
57 changed files with 22226 additions and 7343 deletions

14663
front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -10,25 +10,24 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^17.3.0",
"@angular/cdk": "17.3.4",
"@angular/common": "^17.3.0",
"@angular/compiler": "^17.3.0",
"@angular/core": "^17.3.0",
"@angular/forms": "^17.3.0",
"@angular/material": "17.3.4",
"@angular/material-experimental": "17.3.4",
"@angular/platform-browser": "^17.3.0",
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0",
"@angular/animations": "^19.0.5",
"@angular/cdk": "19.0.4",
"@angular/common": "^19.0.5",
"@angular/compiler": "^19.0.5",
"@angular/core": "^19.0.5",
"@angular/forms": "^19.0.5",
"@angular/material": "19.0.4",
"@angular/platform-browser": "^19.0.5",
"@angular/platform-browser-dynamic": "^19.0.5",
"@angular/router": "^19.0.5",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.3.4",
"@angular/cli": "^17.3.4",
"@angular/compiler-cli": "^17.3.0",
"@angular-devkit/build-angular": "^19.0.6",
"@angular/cli": "^19.0.6",
"@angular/compiler-cli": "^19.0.5",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
@ -36,6 +35,6 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.4.2"
"typescript": "~5.6.3"
}
}

12802
front/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
<button mat-icon-button class="favorite-icon" aria-label="icon-button with heart icon">
<mat-icon>favorite</mat-icon>
</button>
<span>NF Repo</span>
<span>repo.me</span>
<div style="margin-left:auto;">
<button mat-icon-button matTooltip="帮我下载镜像" (click)="downloadImage()">
@ -29,9 +29,9 @@
<td mat-cell *matCellDef="let element">
<button mat-icon-button aria-label="expand row" (click)="showTags($event, element)">
@if (expandedElement === element) {
<mat-icon>keyboard_arrow_up</mat-icon>
<mat-icon>keyboard_arrow_up</mat-icon>
} @else {
<mat-icon>keyboard_arrow_down</mat-icon>
<mat-icon>keyboard_arrow_down</mat-icon>
}
</button>
</td>
@ -74,12 +74,11 @@
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let element" [attr.colspan]="columnsToDisplayWithExpand.length">
<div class="repo-element-detail"
[@detailExpand]="element == expandedElement ? 'expanded' : 'collapsed'">
<div class="repo-element-detail" [@detailExpand]="element == expandedElement ? 'expanded' : 'collapsed'">
@if (element.Tags) {
@for (tag of element.Tags.list; track tag) {
<mat-chip-option color="primary">{{ tag.tag }}</mat-chip-option>
}
@for (tag of element.Tags.list; track tag) {
<mat-chip-option color="primary">{{ tag.tag }}</mat-chip-option>
}
}
</div>
</td>
@ -87,11 +86,10 @@
<tr mat-header-row *matHeaderRowDef="columnsToDisplayWithExpand"></tr>
<tr mat-row *matRowDef="let element; columns: columnsToDisplayWithExpand;"
class="repo-element-row"
[class.repo-expanded-row]="expandedElement === element"
(click)="expandedElement = expandedElement === element ? null : element">
<tr mat-row *matRowDef="let element; columns: columnsToDisplayWithExpand;" class="repo-element-row"
[class.repo-expanded-row]="expandedElement === element"
(click)="expandedElement = expandedElement === element ? null : element">
</tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="repo-detail-row"></tr>
</table>
</div>
</div>

View File

@ -1,14 +1,14 @@
import {Component} from '@angular/core';
import {Repo} from "./interface/repo";
import {RepoService} from "./service/repo.service";
import {FormControl, ReactiveFormsModule} from "@angular/forms";
import {MatToolbar} from "@angular/material/toolbar";
import {MatIconButton} from "@angular/material/button";
import {MatFormField, MatLabel, MatSuffix} from "@angular/material/form-field";
import {MatTooltip} from "@angular/material/tooltip";
import {MatIcon} from "@angular/material/icon";
import {MatInput} from "@angular/material/input";
import {Clipboard} from '@angular/cdk/clipboard';
import { Component } from '@angular/core';
import { Repo } from "./interface/repo";
import { RepoService } from "./service/repo.service";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { MatToolbar } from "@angular/material/toolbar";
import { MatIconButton } from "@angular/material/button";
import { MatFormField, MatLabel, MatSuffix } from "@angular/material/form-field";
import { MatTooltip } from "@angular/material/tooltip";
import { MatIcon } from "@angular/material/icon";
import { MatInput } from "@angular/material/input";
import { Clipboard } from '@angular/cdk/clipboard';
import {
MatCell,
MatCellDef,
@ -18,16 +18,15 @@ import {
MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef,
MatTable
} from "@angular/material/table";
import {DatePipe} from "@angular/common";
import {animate, state, style, transition, trigger} from "@angular/animations";
import {MatChipOption} from "@angular/material/chips";
import {MatDialog} from "@angular/material/dialog";
import {DownloadImageComponent} from "./component/download-image/download-image.component";
import {CapacityPipe} from "./pipe/capacity.pipe";
import { DatePipe } from "@angular/common";
import { animate, state, style, transition, trigger } from "@angular/animations";
import { MatChipOption } from "@angular/material/chips";
import { MatDialog } from "@angular/material/dialog";
import { DownloadImageComponent } from "./component/download-image/download-image.component";
import { CapacityPipe } from "./pipe/capacity.pipe";
@Component({
selector: 'app-root',
standalone: true,
imports: [
MatToolbar,
MatIconButton,
@ -56,11 +55,11 @@ import {CapacityPipe} from "./pipe/capacity.pipe";
styleUrl: './app.component.scss',
animations: [
trigger('detailExpand', [
state('collapsed', style({height: '0px', minHeight: '0'})),
state('expanded', style({height: '*'})),
state('collapsed', style({ height: '0px', minHeight: '0' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]),
],
]
})
export class AppComponent {
title = 'front';
@ -91,10 +90,10 @@ export class AppComponent {
}
copyImageCommand(item: Repo) {
this.clipboard.copy(`docker pull ${this.repo_svc.settings().base_address}/${item.repo}:${item.tag}`)
this.clipboard.copy(`docker pull ${this.repo_svc.settings().repo_name}/${item.repo}:${item.tag}`)
}
downloadImage() {
this.dialog.open(DownloadImageComponent, {data: {}})
this.dialog.open(DownloadImageComponent, { data: {} })
}
}

View File

@ -3,7 +3,7 @@ import {provideRouter} from '@angular/router';
import {routes} from './app.routes';
import {alerterInterceptor} from "./interceptor/alerter.interceptor";
import {HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi} from "@angular/common/http";
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http";
import {provideAnimations} from "@angular/platform-browser/animations";

View File

@ -7,7 +7,8 @@
<mat-dialog-content class="mat-typography">
<p>
<mat-form-field appearance="outline">
<mat-label>云上镜像名称</mat-label>
<mat-label style="margin-left: 1.5rem">云上镜像名称</mat-label>
<span matTextPrefix>library/</span>
<input matInput placeholder="golang:1.22.2-alpine3.19" [(ngModel)]="source">
<mat-icon matSuffix>clear</mat-icon>
</mat-form-field>
@ -15,7 +16,7 @@
<p>
<mat-form-field appearance="outline">
<mat-label style="margin-left: 1.5rem">本地镜像名称</mat-label>
<span matTextPrefix>{{ repo_svc.settings().base_address + '/ ' }}</span>
<span matTextPrefix>{{ repo_svc.settings().repo_name + '/ ' }}</span>
<input matInput placeholder="external/golang:latest" [(ngModel)]="target">
<mat-icon matSuffix>clear</mat-icon>
</mat-form-field>
@ -33,4 +34,4 @@
<button mat-button cdkFocusInitial color="primary" (click)="proxyDownload()">下载</button>
</mat-dialog-actions>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
import {Component} from '@angular/core';
import { Component } from '@angular/core';
import {
MatDialogActions,
MatDialogClose,
@ -6,20 +6,19 @@ import {
MatDialogRef,
MatDialogTitle
} from "@angular/material/dialog";
import {MatButton} from "@angular/material/button";
import {MatFormField, MatLabel, MatPrefix, MatSuffix} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {MatIcon} from "@angular/material/icon";
import {RepoService} from "../../service/repo.service";
import {HttpClient} from "@angular/common/http";
import {FormsModule} from "@angular/forms";
import {CdkConnectedOverlay, CdkOverlayOrigin} from "@angular/cdk/overlay";
import {MatProgressSpinner} from "@angular/material/progress-spinner";
import {MsgService} from "../../service/msg.service";
import { MatButton } from "@angular/material/button";
import { MatFormField, MatLabel, MatPrefix, MatSuffix } from "@angular/material/form-field";
import { MatInput } from "@angular/material/input";
import { MatIcon } from "@angular/material/icon";
import { RepoService } from "../../service/repo.service";
import { HttpClient } from "@angular/common/http";
import { FormsModule } from "@angular/forms";
import { CdkConnectedOverlay, CdkOverlayOrigin } from "@angular/cdk/overlay";
import { MatProgressSpinner } from "@angular/material/progress-spinner";
import { MsgService } from "../../service/msg.service";
@Component({
selector: 'app-download-image',
standalone: true,
imports: [
MatDialogTitle,
MatDialogContent,
@ -33,9 +32,7 @@ import {MsgService} from "../../service/msg.service";
MatFormField,
MatPrefix,
FormsModule,
CdkConnectedOverlay,
MatProgressSpinner,
CdkOverlayOrigin
],
templateUrl: './download-image.component.html',
styleUrl: './download-image.component.scss'
@ -48,7 +45,7 @@ export class DownloadImageComponent {
title = '下载云上镜像'
inProgress = false
readonly url_proxy_download = '/api/repo/proxy'
readonly url_proxy_download = '/_api/repo/proxy'
constructor(
public repo_svc: RepoService,
@ -67,14 +64,14 @@ export class DownloadImageComponent {
async proxyDownload() {
const response = await fetch(this.url_proxy_download, {
method: "POST",
body: JSON.stringify({target: this.target, source: this.source, proxy: this.proxy}),
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ target: this.target, source: this.source, proxy: this.proxy }),
headers: { "Content-Type": "application/json" },
})
const reader = response.body?.pipeThrough(new TextDecoderStream()).getReader()
if (reader) {
this.inProgress = true
while (true) {
const {value, done} = await reader.read()
const { value, done } = await reader.read()
if (done) {
break;
}

View File

@ -1,10 +1,4 @@
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpResponse
} from '@angular/common/http';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import {map, Observable} from "rxjs";
import {Response} from "../interface/response";
import {MsgService} from "../service/msg.service";

View File

@ -17,5 +17,5 @@ export interface Repo {
}
export interface Settings {
base_address: string
repo_name: string
}

View File

@ -95,8 +95,8 @@ export class MsgService {
@Component({
selector: 'snack-message-success',
template: `
selector: 'snack-message-success',
template: `
<div class="snack-message-success snack-message">
<div>
<mat-icon>cancel</mat-icon>
@ -106,11 +106,10 @@ export class MsgService {
</div>
</div>
`,
standalone: true,
imports: [
MatIconModule
],
styles: [`
imports: [
MatIconModule
],
styles: [`
.snack-message-success {
}
`]
@ -126,8 +125,8 @@ export class SnackMessageSuccess {
@Component({
selector: 'snack-message-info',
template: `
selector: 'snack-message-info',
template: `
<div class="snack-message-info snack-message">
<div>
<mat-icon>cancel</mat-icon>
@ -137,11 +136,10 @@ export class SnackMessageSuccess {
</div>
</div>
`,
standalone: true,
imports: [
MatIconModule
],
styles: [`
imports: [
MatIconModule
],
styles: [`
.snack-message-info {
}
`]
@ -156,8 +154,8 @@ export class SnackMessageInfo {
}
@Component({
selector: 'snack-message-warning',
template: `
selector: 'snack-message-warning',
template: `
<div class="snack-message-warning snack-message">
<div>
<mat-icon>cancel</mat-icon>
@ -167,11 +165,10 @@ export class SnackMessageInfo {
</div>
</div>
`,
standalone: true,
imports: [
MatIconModule
],
styles: [`
imports: [
MatIconModule
],
styles: [`
.snack-message-warning {
}
`]
@ -186,8 +183,8 @@ export class SnackMessageWarning {
}
@Component({
selector: 'snack-message-error',
template: `
selector: 'snack-message-error',
template: `
<div class="snack-message-error snack-message">
<div>
<mat-icon>cancel</mat-icon>
@ -197,11 +194,10 @@ export class SnackMessageWarning {
</div>
</div>
`,
standalone: true,
imports: [
MatIconModule
],
styles: [`
imports: [
MatIconModule
],
styles: [`
`]
})
export class SnackMessageError {

View File

@ -1,8 +1,8 @@
import {Injectable, signal, WritableSignal} from '@angular/core';
import {Repo, Settings} from '../interface/repo'
import {Response} from "../interface/response";
import {HttpClient} from "@angular/common/http";
import {MsgService} from "./msg.service";
import { Injectable, signal, WritableSignal } from '@angular/core';
import { Repo, Settings } from '../interface/repo'
import { Response } from "../interface/response";
import { HttpClient } from "@angular/common/http";
import { MsgService } from "./msg.service";
@Injectable({
providedIn: 'root'
@ -10,14 +10,14 @@ import {MsgService} from "./msg.service";
export class RepoService {
readonly init_catalog = [
{id: 1, created_at: 1713164091576, updated_at: 1713164091576, digest_id: 1, repo: "external/golang", tag: "latest"},
{id: 2, created_at: 1713164091576, updated_at: 1713164091576, digest_id: 2, repo: "external/alpine", tag: "latest"},
{id: 3, created_at: 1713164091576, updated_at: 1713164091576, digest_id: 3, repo: "external/nginx", tag: "latest"},
{ id: 1, created_at: 1713164091576, updated_at: 1713164091576, digest_id: 1, repo: "external/golang", tag: "latest" },
{ id: 2, created_at: 1713164091576, updated_at: 1713164091576, digest_id: 2, repo: "external/alpine", tag: "latest" },
{ id: 3, created_at: 1713164091576, updated_at: 1713164091576, digest_id: 3, repo: "external/nginx", tag: "latest" },
] as Repo[]
readonly url_settings = '/api/repo/settings'
readonly url_catalog = '/api/repo/list'
readonly url_tag = '/api/repo/tag/list'
readonly url_settings = '/_api/repo/settings'
readonly url_catalog = '/_api/repo/list'
readonly url_tag = '/_api/repo/tag/list'
settings = signal({} as Settings)
@ -58,7 +58,7 @@ export class RepoService {
}).subscribe(res => {
if (res.status === 200) {
this.catalog.update(v => {
return {...v, limit: limit, last: last, keyword: keyword, list: res.data.list, total: res.data.total}
return { ...v, limit: limit, last: last, keyword: keyword, list: res.data.list, total: res.data.total }
})
return

View File

@ -1,15 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Front</title>
<title>❤repo</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>
</html>

View File

@ -1,6 +1,6 @@
{
"/api": {
"target": "https://repo.me",
"/_api": {
"target": "http://127.0.0.1:8383",
"secure": false
}
}
}

View File

@ -29,4 +29,4 @@
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
}