Merge branch 'master' into form-login-register
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details

pull/14/head
Clément FRÉVILLE 1 year ago
commit 3067443127

@ -9,6 +9,13 @@ steps:
- npm install
- npm run build
- name: test-chrome
image: timbru31/node-chrome:20-slim
commands:
- npm run test -- --browsers=ChromeHeadlessCI --watch=false
depends_on:
- build
- name: sonar
image: sonarsource/sonar-scanner-cli:5
commands:

1
.gitignore vendored

@ -19,6 +19,7 @@ yarn-error.log
*.launch
.settings/
*.sublime-workspace
.nx
# Visual Studio Code
.vscode/*

@ -16,9 +16,14 @@
"outputPath": "dist/sandkasten",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss"
@ -27,6 +32,12 @@
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"budgets": [
{
"type": "initial",
@ -42,6 +53,7 @@
"outputHashing": "all"
},
"development": {
"fileReplacements": [],
"optimization": false,
"extractLicenses": false,
"sourceMap": true
@ -70,20 +82,30 @@
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": ["src/favicon.ico", "src/assets"],
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss"
],
"scripts": []
"scripts": [],
"karmaConfig": "karma.conf.js"
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
@ -91,6 +113,8 @@
},
"cli": {
"analytics": false,
"schematicCollections": ["@angular-eslint/schematics"]
"schematicCollections": [
"@angular-eslint/schematics"
]
}
}

@ -0,0 +1,46 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-firefox-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/sandkasten'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
browsers: ['Chrome', 'Firefox'],
restartOnFileChange: true,
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
}
});
};

54
package-lock.json generated

@ -18,8 +18,11 @@
"@angular/platform-browser": "^17.3.7",
"@angular/platform-browser-dynamic": "^17.3.7",
"@angular/router": "^17.3.7",
"@codemirror/collab": "^6.1.1",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/language": "^6.10.2",
"@codemirror/legacy-modes": "^6.4.0",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.26.3",
"@emailjs/browser": "^4.3.3",
@ -50,6 +53,7 @@
"karma": "~6.4.2",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-firefox-launcher": "^2.1.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.2.5",
@ -2512,6 +2516,15 @@
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/collab": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@codemirror/collab/-/collab-6.1.1.tgz",
"integrity": "sha512-tkIn9Jguh98ie12dbBuba3lE8LHUkaMrIFuCVeVGhncSczFdKmX25vC12+58+yqQW5AXi3py6jWY0W+jelyglA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.5.0.tgz",
@ -2547,9 +2560,9 @@
}
},
"node_modules/@codemirror/language": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz",
"integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==",
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz",
"integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@ -2559,6 +2572,14 @@
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/legacy-modes": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.4.0.tgz",
"integrity": "sha512-5m/K+1A6gYR0e+h/dEde7LoGimMjRtWXZFg4Lo70cc8HzjSdHe3fLwjWMR0VRl5KFT1SxalSap7uMgPKF28wBA==",
"dependencies": {
"@codemirror/language": "^6.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.7.0.tgz",
@ -10467,6 +10488,33 @@
"node": "*"
}
},
"node_modules/karma-firefox-launcher": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.3.tgz",
"integrity": "sha512-LMM2bseebLbYjODBOVt7TCPP9OI2vZIXCavIXhkO9m+10Uj5l7u/SKoeRmYx8FYHTVGZSpk6peX+3BMHC1WwNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-wsl": "^2.2.0",
"which": "^3.0.0"
}
},
"node_modules/karma-firefox-launcher/node_modules/which": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz",
"integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/which.js"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/karma-jasmine": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz",

@ -22,8 +22,11 @@
"@angular/platform-browser": "^17.3.7",
"@angular/platform-browser-dynamic": "^17.3.7",
"@angular/router": "^17.3.7",
"@codemirror/collab": "^6.1.1",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/language": "^6.10.2",
"@codemirror/legacy-modes": "^6.4.0",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.26.3",
"@emailjs/browser": "^4.3.3",
@ -54,6 +57,7 @@
"karma": "~6.4.2",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-firefox-launcher": "^2.1.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.2.5",

@ -1,11 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import { TranslateModule } from '@ngx-translate/core';
import { RouterModule } from '@angular/router';
describe('AppComponent', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [RouterTestingModule, AppComponent],
imports: [
RouterModule.forRoot([]),
AppComponent,
TranslateModule.forRoot(),
],
})
);
@ -19,8 +24,6 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain(
'sandkasten app is running!'
);
expect(compiled.textContent).toContain('HeaderPage.Editor');
});
});

@ -1,5 +1,4 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Routes } from '@angular/router';
import { EditorComponent } from './components/editor/editor.component';
import { LandingPageComponent } from './components/landing-page/landing-page.component';
import { DocumentationComponent } from './components/documentation/documentation.component';
@ -11,9 +10,10 @@ import { RegisterComponent } from './components/register/register.component';
import { LoginComponent } from './components/login/login.component';
// Toutes les routes de l'application sont définies ici
const routes: Routes = [
export const routes: Routes = [
{ path: '', component: LandingPageComponent },
{ path: 'editor', component: EditorComponent },
{ path: 'editor-live/:idRoom', component: EditorComponent },
{ path: 'documentation', component: DocumentationComponent },
{ path: 'contact', component: FormComponent },
{ path: 'our-story', component: OurStoryComponent },
@ -22,9 +22,3 @@ const routes: Routes = [
{ path: 'register', component: RegisterComponent },
{ path: 'login', component: LoginComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

@ -59,6 +59,10 @@
</div>
}
<button class="button-join" (click)="onCreateRoomButtonClicked()">
Créer une salle
</button>
<select id="language" [(ngModel)]="selectedLanguage">
@for (language of languages; track language.name) {
<option [ngValue]="language">{{ language.name }}</option>
@ -68,7 +72,6 @@
<div class="param-editor">
<button
class="button-icon button-run"
type="button"
(click)="onRunButtonClicked()"
[disabled]="isLoaded">
<div>RUN</div>

@ -40,6 +40,16 @@ svg {
border-radius: 10px;
}
.button-join {
background-color: #1c53bf;
border: none;
color: white;
padding: 12px 16px;
font-size: 16px;
cursor: pointer;
border-radius: 10px;
}
select {
background-color: #0000f0;
border: none;

@ -1,11 +1,12 @@
import { Component, ViewChild } from '@angular/core';
import { CodeExecutionService } from 'src/app/services/codeExecution.service';
import { Compartment } from '@codemirror/state';
import { Component, Input, ViewChild } from '@angular/core';
import { BackendService } from 'src/app/services/backendService.service';
import { Compartment, StateEffect } from '@codemirror/state';
import { CodeMirrorComponent } from '@sandkasten/codemirror6-editor';
import { LanguageDescription } from '@codemirror/language';
import { CODE_DEFAULTS, LANGUAGES } from '../languages';
import { SafeHTMLPipe } from '../../safe-html.pipe';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import {
keymap,
highlightSpecialChars,
@ -36,6 +37,12 @@ import {
closeBracketsKeymap,
} from '@codemirror/autocomplete';
import { lintKeymap } from '@codemirror/lint';
import {
Connection,
getDocument,
peerExtension,
} from '../../services/connection.service';
import { environment } from '../../../environments/environment';
const basicSetup: Extension = (() => [
highlightActiveLineGutter(),
@ -88,6 +95,7 @@ export class EditorComponent {
get selectedLanguage(): LanguageDescription {
return this._selectedLanguage;
}
set selectedLanguage(value: LanguageDescription) {
this._selectedLanguage = value;
if (value.name in CODE_DEFAULTS) {
@ -100,10 +108,12 @@ export class EditorComponent {
});
});
}
private _linesNumbers: boolean = true;
get linesNumbers() {
return this._linesNumbers;
}
set linesNumbers(lines: boolean) {
this._linesNumbers = lines;
this.codemirror.editor?.dispatch({
@ -131,21 +141,58 @@ export class EditorComponent {
this.languageCompartment.of(this.selectedLanguage.support!),
];
constructor(private codeExecutionService: CodeExecutionService) {}
private client: WebSocket | undefined;
@Input()
set idRoom(idRoom: string) {
if (idRoom === undefined) {
return;
}
this.client = new WebSocket(`${environment.webSocketUrl}/live/${idRoom}`);
this.client.addEventListener('open', async () => {
let conn = new Connection(this.client!);
let { version, doc } = await getDocument(conn);
this.codemirror.editor?.dispatch({
changes: {
from: 0,
to: this.codemirror.editor.state.doc.length,
insert: doc,
},
});
this.codemirror.editor?.dispatch({
effects: StateEffect.appendConfig.of([peerExtension(version, conn)]),
});
});
}
constructor(
private router: Router,
private backendService: BackendService
) {
backendService.getResult().subscribe((msg) => {
if (msg.type === 'stdout' || msg.type === 'stderr') {
this.resultContent += msg.text;
}
});
}
// Efface le contenu de l'éditeur
clear(): void {
this.editorContent = '';
}
async onCreateRoomButtonClicked() {
const idRoom = await this.backendService.createRoom(this.editorContent);
await this.router.navigate([`./editor-live/${idRoom}`]);
}
onRunButtonClicked() {
// Le code à exécuter est le contenu de l'éditeur
const codeToExecute = this.editorContent;
this.codeExecutionService.executeCode(
codeToExecute,
this.selectedLanguage.name
);
this.backendService.executeCode(codeToExecute, this.selectedLanguage.name);
this.resultContent = '';
}

@ -1,6 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
import { TranslateModule } from '@ngx-translate/core';
import { RouterModule } from '@angular/router';
describe('HeaderComponent', () => {
let component: HeaderComponent;
@ -8,7 +10,11 @@ describe('HeaderComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HeaderComponent],
imports: [
RouterModule.forRoot([]),
HeaderComponent,
TranslateModule.forRoot(),
],
});
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;

@ -1,6 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LandingPageComponent } from './landing-page.component';
import { TranslateModule } from '@ngx-translate/core';
import { RouterModule } from '@angular/router';
describe('LandingPageComponent', () => {
let component: LandingPageComponent;
@ -8,7 +10,11 @@ describe('LandingPageComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [LandingPageComponent],
imports: [
RouterModule.forRoot([]),
LandingPageComponent,
TranslateModule.forRoot(),
],
});
fixture = TestBed.createComponent(LandingPageComponent);
component = fixture.componentInstance;

@ -1,6 +1,15 @@
import { LanguageDescription } from '@codemirror/language';
import {
LanguageDescription,
LanguageSupport,
StreamLanguage,
StreamParser,
} from '@codemirror/language';
import { javascript } from '@codemirror/lang-javascript';
function legacy(parser: StreamParser<unknown>): LanguageSupport {
return new LanguageSupport(StreamLanguage.define(parser));
}
export const CODE_DEFAULTS = {
C: /** @lang C */ `#include <stdio.h>
int main() {
@ -14,6 +23,7 @@ int main() {
}`,
JavaScript: /** @lang JS */ `console.log("Hello, World!");`,
TypeScript: /** @lang TS */ `console.log("Hello, World!");`,
Bash: 'echo "Hello, world!"',
};
export const LANGUAGES = [
@ -48,4 +58,15 @@ export const LANGUAGES = [
);
},
}),
LanguageDescription.of({
name: 'Bash',
alias: ['bash', 'sh', 'sh'],
extensions: ['sh', 'ksh', 'bash'],
filename: /^PKGBUILD$/,
load() {
return import('@codemirror/legacy-modes/mode/shell').then((m) =>
legacy(m.shell)
);
},
}),
];

@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PrivacyPolicyComponent } from './privacy-policy.component';
import { TranslateModule } from '@ngx-translate/core';
describe('PrivacyPolicyComponent', () => {
let component: PrivacyPolicyComponent;
@ -8,7 +9,7 @@ describe('PrivacyPolicyComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PrivacyPolicyComponent],
imports: [PrivacyPolicyComponent, TranslateModule.forRoot()],
}).compileComponents();
fixture = TestBed.createComponent(PrivacyPolicyComponent);

@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TermsOfServiceComponent } from './terms-of-service.component';
import { TranslateModule } from '@ngx-translate/core';
describe('TermsOfServiceComponent', () => {
let component: TermsOfServiceComponent;
@ -8,7 +9,7 @@ describe('TermsOfServiceComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TermsOfServiceComponent],
imports: [TermsOfServiceComponent, TranslateModule.forRoot()],
}).compileComponents();
fixture = TestBed.createComponent(TermsOfServiceComponent);

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { SSE } from 'sse.js';
import { Observable, Subject } from 'rxjs';
import { environment } from '../../environments/environment';
export type ExecutionMessage = {
type: 'stdout' | 'stderr' | 'exit';
@ -10,14 +11,26 @@ export type ExecutionMessage = {
@Injectable({
providedIn: 'root',
})
export class CodeExecutionService {
private apiUrl = 'http://localhost:3000/run';
export class BackendService {
private apiUrl = environment.apiUrl;
private resultSubject = new Subject<ExecutionMessage>();
constructor() {}
async createRoom(code: string) {
const reponse = await fetch(`${this.apiUrl}/live`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code }),
});
return reponse.text();
}
executeCode(code: string, language: string) {
const sse = new SSE(this.apiUrl, {
const sse = new SSE(`${this.apiUrl}/run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

@ -0,0 +1,120 @@
import { ChangeSet, Text } from '@codemirror/state';
import { EditorView } from 'codemirror';
import {
Update,
collab,
getSyncedVersion,
receiveUpdates,
sendableUpdates,
} from '@codemirror/collab';
import { ViewPlugin, ViewUpdate } from '@codemirror/view';
export class Connection {
private requestId = 0;
private resolves: Record<number, (value: any) => void> = {};
constructor(private client: WebSocket) {
client.addEventListener('message', (event) => {
const response = JSON.parse(event.data);
if ('_request' in response) {
const resolve = this.resolves[response._request];
if (resolve) {
resolve(response.payload);
} else {
console.error(
'Received response for unknown or already used request',
response._request
);
}
} else {
console.error('Received invalid response', response._request);
}
});
}
request(body: Record<string, unknown>): Promise<any> {
body['_request'] = this.requestId;
this.client.send(JSON.stringify(body));
return new Promise(
(resolve) => (this.resolves[this.requestId++] = resolve)
);
}
}
function pushUpdates(
connection: Connection,
version: number,
fullUpdates: readonly Update[]
): Promise<boolean> {
// Strip off transaction data
let updates = fullUpdates.map((u) => ({
clientID: u.clientID,
changes: u.changes.toJSON(),
}));
return connection.request({ type: 'pushUpdates', version, updates });
}
function pullUpdates(
connection: Connection,
version: number
): Promise<readonly Update[]> {
return connection.request({ type: 'pullUpdates', version }).then((updates) =>
updates.map((u: any) => ({
changes: ChangeSet.fromJSON(u.changes),
clientID: u.clientID,
}))
);
}
export function getDocument(
connection: Connection
): Promise<{ version: number; doc: Text }> {
return connection.request({ type: 'getDocument' }).then((data) => ({
version: data.version,
doc: Text.of(data.doc.split('\n')),
}));
}
export function peerExtension(startVersion: number, connection: Connection) {
let plugin = ViewPlugin.fromClass(
class {
private pushing = false;
private done = false;
constructor(private view: EditorView) {
this.pull();
}
update(update: ViewUpdate) {
if (update.docChanged) this.push();
}
async push() {
let updates = sendableUpdates(this.view.state);
if (this.pushing || !updates.length) return;
this.pushing = true;
let version = getSyncedVersion(this.view.state);
await pushUpdates(connection, version, updates);
this.pushing = false;
// Regardless of whether the push failed or new updates came in
// while it was running, try again if there's updates remaining
if (sendableUpdates(this.view.state).length)
setTimeout(() => this.push(), 100);
}
async pull() {
while (!this.done) {
let version = getSyncedVersion(this.view.state);
let updates = await pullUpdates(connection, version);
this.view.dispatch(receiveUpdates(this.view.state, updates));
}
}
destroy() {
this.done = true;
}
}
);
return [collab({ startVersion }), plugin];
}

@ -0,0 +1,5 @@
export const environment = {
production: true,
apiUrl: 'https://tododomain.com',
webSocketUrl: 'ws://tododomain.com',
};

@ -0,0 +1,5 @@
export const environment = {
production: false,
apiUrl: 'http://localhost:3000',
webSocketUrl: 'ws://localhost:3000',
};

@ -8,16 +8,17 @@ import {
HttpClient,
} from '@angular/common/http';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app/app-routing.module';
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser';
import { TranslationService } from './app/services/translation.service';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withComponentInputBinding()),
importProvidersFrom(
BrowserModule,
AppRoutingModule,
ReactiveFormsModule,
FormsModule,
TranslateModule.forRoot({

Loading…
Cancel
Save