Skip to content

Commit

Permalink
feat: projects exchange
Browse files Browse the repository at this point in the history
  • Loading branch information
navix committed May 2, 2024
1 parent 5fab380 commit 5085a6a
Show file tree
Hide file tree
Showing 20 changed files with 272 additions and 29 deletions.
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"exchange:start": "node dist/exchange/exchange/app",
"exchange:build": "tsc --project ./projects/exchange/tsconfig.json",
"exchange:build:watch": "rimraf ./dist/exchange && tsc --project ./projects/exchange/tsconfig.json --watch",
"exchange:deploy": "docker build -t ghcr.io/navix/ngxe/exchange -f projects/exchange/Dockerfile . && docker push ghcr.io/navix/ngxe/exchange",
"release:build": "npm run ngxe:build && npm run backend:build",
"release": "npm run release:build && node release.js"
},
Expand All @@ -30,6 +31,7 @@
"@angular/platform-browser": "^17.3.7",
"@angular/platform-browser-dynamic": "^17.3.7",
"@angular/router": "^17.3.7",
"@fastify/cors": "^9.0.1",
"@fastify/static": "^7.0.3",
"ajv": "^8.13.0",
"better-ajv-errors": "1.2.0",
Expand Down
22 changes: 22 additions & 0 deletions projects/exchange/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM node:20.12.2 as dist
WORKDIR /tmp/
COPY package.json package-lock.json tsconfig.json ./
RUN npm ci --force
COPY projects/exchange/ projects/exchange/
COPY projects/meta/ projects/meta/
RUN npm run exchange:build

FROM node:20.12.2-slim as node_modules
WORKDIR /tmp/
COPY package.json package-lock.json ./
RUN npm install --production --force

FROM node:20.12.2
RUN npm install pm2 -g
RUN mkdir -p /var/app
WORKDIR /var/app
COPY --from=node_modules /tmp/node_modules ./node_modules
COPY --from=dist /tmp/dist/exchange/ ./dist/
COPY projects/exchange/docker-entrypoint.sh ./
EXPOSE 3080
ENTRYPOINT ["./docker-entrypoint.sh"]
51 changes: 51 additions & 0 deletions projects/exchange/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fastify from 'fastify';
import {existsSync, readFileSync, writeFileSync, mkdirSync} from 'fs';
import {resolve} from 'path';
import {Api_GetProject} from '../meta/api';
import {formatExchangeFile} from '../meta/format-exchange-file';

const app = fastify({
logger: true,
bodyLimit: 10 * 10000, // X * MB
});

const projectsPath = resolve(__dirname, 'projects');
if (!existsSync(projectsPath)) {
mkdirSync(projectsPath);
}

app.register(async app => {
app.post<{Body: Api_GetProject; Params: {project: string; branch: string}}>(
'/api/project/save/:project/:branch',
async (req) => {
const path = resolve(projectsPath, compileProjectFilename(req.params.project, req.params.branch));
writeFileSync(path, JSON.stringify(req.body));
return true;
});

app.post<{Params: {project: string; branch: string}}>(
'/api/project/load/:project/:branch',
async (req) => {
const path = resolve(projectsPath, compileProjectFilename(req.params.project, req.params.branch));
if (!existsSync(path)) {
throw new Error(`File for project/branch not found`);
}
return JSON.parse(readFileSync(path, {encoding: 'utf8'}));
});
});

app.register(require('fastify-disablecache'));
app.register(require('@fastify/cors'));

app.listen({
port: 3080,
host: '0.0.0.0',
}).then(async (url) => {
app.log.info(`🗃️ ngxe exchange working on ${url}`);
}).catch(err => {
console.error(err);
})

function compileProjectFilename(project: string, branch: string) {
return `proj_${formatExchangeFile(project)}-branch_${formatExchangeFile(branch)}`;
}
3 changes: 3 additions & 0 deletions projects/exchange/docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

pm2-docker ./dist/exchange/app.js --name='app'
2 changes: 2 additions & 0 deletions projects/exchange/projects/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
27 changes: 27 additions & 0 deletions projects/exchange/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"module": "node16",
"target": "ES2022",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"declaration": false,
"incremental": true,
"outDir": "../../dist/exchange",
"baseUrl": "./",
"resolveJsonModule": true,
"strict": true,
"alwaysStrict": true,
"allowJs": true,
"esModuleInterop": true,
"typeRoots": ["node_modules/@types"],
"skipLibCheck": true,
"strictPropertyInitialization": false,
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"lib": ["ES2023"]
},
"exclude": [
"node_modules",
"dist"
]
}
3 changes: 3 additions & 0 deletions projects/meta/format-exchange-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function formatExchangeFile(file: string = '') {
return file.replace(/[^a-z0-9]/gi, '_');
}
33 changes: 16 additions & 17 deletions projects/ngxe/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ <h1>ngxe editor</h1>
<ng-container *ngIf="project.data && project.currentTranslation">
<div class="head">
<div class="main">
<h1 class="title">{{ project.data.config.name }}</h1>
<h1 class="title">{{ project.data.config.name }} / {{ project.data.branch }}</h1>
<div class="actions">
Locales:
<button
Expand All @@ -30,23 +30,22 @@ <h1 class="title">{{ project.data.config.name }}</h1>
>
{{ translation.locale }}
</button>
<ng-container *ngIf="backendless">
<button (click)="export()">💾 Save Project</button>
</ng-container>
<ng-container *ngIf="!backendless">
@if (!backendless) {
<button (click)="save()">💾 Save Project</button>
<button (click)="export()">📤 Export</button>
<button appFileHolder>
<input
(select)="import($event)"
appFile
type="file"
>
📥 Import
</button>
</ng-container>
<div>
</div>
}
|
<button (click)="saveToExchange()">📤 Save to Exchange</button>
<button (click)="loadFromExchange()">📥 Load from Exchange</button>
|
<button (click)="export()">💾 Save to file</button>
<button appFileHolder>
<input
(select)="import($event)"
appFile
type="file"
>
📂 Load from file
</button>
</div>
</div>
<div class="side">
Expand Down
72 changes: 64 additions & 8 deletions projects/ngxe/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Component, HostBinding, OnInit } from '@angular/core';
import {Component, HostBinding, inject, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import { saveAs } from 'file-saver';
import { finalize } from 'rxjs/operators';
import {EMPTY} from 'rxjs';
import {catchError, finalize} from 'rxjs/operators';
import {formatExchangeFile} from '../../../meta/format-exchange-file';
import { environment } from '../environments/environment';
import {ExchangeService} from './exchange.service';
import { Project } from './project';

const themeStorageKey = '_THEME';
Expand All @@ -12,6 +16,9 @@ const themeStorageKey = '_THEME';
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
exchange = inject(ExchangeService);
route = inject(ActivatedRoute);

backendless = environment.backendless;

loading = false;
Expand All @@ -21,17 +28,23 @@ export class AppComponent implements OnInit {
constructor(
public project: Project,
) {
// load local settings
const theme = localStorage.getItem(themeStorageKey);
if (theme) {
this.themeClass = theme;
}
}

ngOnInit() {
const {exp, exb} = this.route.snapshot.queryParams;
if (exp && exb) {
this.loadFromExchange(exp, exb, true);
return;
}

if (!this.backendless) {
this.loadFromBackend();
}
// load local settings
const theme = localStorage.getItem(themeStorageKey);
if (theme) {
this.themeClass = theme;
}
}

@HostBinding('class') get classBinding() {
Expand Down Expand Up @@ -67,7 +80,7 @@ export class AppComponent implements OnInit {

async import(files: any[]) {
try {
this.project.import(JSON.parse(files[0].data));
this.project.mergeImport(JSON.parse(files[0].data));
} catch (e: any) {
alert('Error: ' + e.error);
}
Expand Down Expand Up @@ -97,4 +110,47 @@ export class AppComponent implements OnInit {
},
);
}

saveToExchange() {
this.exchange
.saveProject({
project: this.project.data!.config.name,
branch: this.project.data!.branch || '',
body: this.project.data,
})
.subscribe(res => {
if (res) {
alert(`Project saved. Sharing URL: https://ngxe.oleksanovyk.com/?exp=${formatExchangeFile(this.project.data!.config.name)}&exb=${formatExchangeFile(this.project.data!.branch)}`);
} else {
alert('Save failed!');
}
});
}

loadFromExchange(project?: string, branch?: string, force = false) {
if (!force && !confirm('Current unsaved changes will be lost. Continue?')) {
return;
}
this.loading = true;
this.exchange
.loadProject({
project: project || this.project.data!.config.name,
branch: branch || this.project.data!.branch || '',
})
.pipe(
finalize(() => this.loading = false),
catchError(err => {
alert(`Exchange API error!`);
console.error(err);
return EMPTY;
}),
)
.subscribe(res => {
if (res.success) {
this.project.fullImport(res)
} else {
alert(`Exchange API error!`);
}
});
}
}
11 changes: 10 additions & 1 deletion projects/ngxe/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import {RouterModule} from '@angular/router';
import { AppComponent } from './app.component';
import { PlaceholdersValidatorDirective } from './placeholders/placeholders-validator.directive';
import {RootComponent} from './root/root.component';
import { TableComponent } from './table/table.component';
import { FileComponent } from './file/file.component';
import { FileHolderDirective } from './file/file-holder.directive';

@NgModule({
declarations: [
RootComponent,
AppComponent,
TableComponent,
PlaceholdersValidatorDirective,
Expand All @@ -20,9 +23,15 @@ import { FileHolderDirective } from './file/file-holder.directive';
BrowserModule,
HttpClientModule,
FormsModule,
RouterModule.forRoot([
{
path: '',
component: AppComponent,
}
])
],
providers: [],
bootstrap: [AppComponent],
bootstrap: [RootComponent],
})
export class AppModule {
}
23 changes: 23 additions & 0 deletions projects/ngxe/src/app/exchange.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Api_Error, Api_GetProject } from '../../../meta/api';
import {environment} from '../environments/environment';

@Injectable({
providedIn: 'root',
})
export class ExchangeService {
constructor(
private http: HttpClient,
) {
}

loadProject({project, branch}: {project: string; branch: string}): Observable<Api_GetProject | Api_Error> {
return this.http.post<Api_GetProject | Api_Error>(`${environment.exchangeUrl}/api/project/load/${project}/${branch}`, {});
}

saveProject({project, branch, body}: {project: string; branch: string, body: any}): Observable<true | Api_Error> {
return this.http.post<true | Api_Error>(`${environment.exchangeUrl}/api/project/save/${project}/${branch}`, body);
}
}
Loading

0 comments on commit 5085a6a

Please sign in to comment.