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:
14663
front/package-lock.json
generated
Normal file
14663
front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
12802
front/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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>
|
@ -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: {} })
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -17,5 +17,5 @@ export interface Repo {
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
base_address: string
|
||||
repo_name: string
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"/api": {
|
||||
"target": "https://repo.me",
|
||||
"/_api": {
|
||||
"target": "http://127.0.0.1:8383",
|
||||
"secure": false
|
||||
}
|
||||
}
|
||||
}
|
@ -29,4 +29,4 @@
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user