diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..cc4c675 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,198 @@ +pipeline { + agent any + + environment { + DatabaseUrl = sh(returnStdout: true, script: 'bash /var/lib/jenkins/variables/CureNet/var.sh ${BRANCH_NAME} DatabaseUrl').trim() + ProjectPath = sh(returnStdout: true, script: 'bash /var/lib/jenkins/variables/CureNet/var.sh ${BRANCH_NAME} ProjectPath').trim() + DatabaseUrlTesting = sh(returnStdout: true, script: 'bash /var/lib/jenkins/variables/CureNet/var.sh master DatabaseUrl').trim() + } + + stages { + stage('Pull') { + steps { + git(url: 'http://git.fufle.net/Community/CureNet.git', branch: '${BRANCH_NAME}', credentialsId: '799188a2-c281-4e4c-b78f-c82132ea792b', poll: true) + } + } + + stage('Approve') { + steps { + echo "http://git.fufle.net/Community/CureNet/commit/${env.GIT_COMMIT}" + script { + if (env.BRANCH_NAME == "prod") { + echo "http://git.fufle.net/Community/CureNet/compare/prod...staging" + input(message: 'Deploy to production?', id: 'A', ok: 'Yes', submitter: 'deploy', submitterParameter: 'a') + } + if (env.BRANCH_NAME == "staging") { + echo "http://git.fufle.net/Community/CureNet/compare/staging...beta" + input(message: 'Deploy to staging instance?', id: 'A', ok: 'Yes', submitter: 'deploy', submitterParameter: 'a') + } + if (env.BRANCH_NAME == "beta") { + echo "http://git.fufle.net/Community/CureNet/compare/beta...dev" + input(message: 'Deploy to beta instance?', id: 'A', ok: 'Yes', submitter: 'deploy', submitterParameter: 'a') + } + } + + } + } + + stage('Install') { + parallel { + stage('Frontend') { + steps { + dir(path: 'src/frontend') { + sh 'sed -i "s/-1001/${BUILD_ID}/g" src/environments/*.ts' + sh 'Timestamp=`php8.0 -r "echo time();"` && sed -i "s/-1002/${Timestamp}/g" src/environments/*.ts' + sh 'npm ci' + } + + } + } + + stage('Backend') { + steps { + dir(path: 'src/backend') { + sh 'php8.0 /bin/composer install' + } + + } + } + + } + } + + stage('Configure for tests') { + parallel { + stage('Frontend') { + steps { + dir(path: 'src/frontend') { + sh 'echo skip' + } + + } + } + + stage('Backend') { + steps { + dir(path: 'src/backend') { + sh 'APP_SECRET=`php8.0 -r "echo md5(\\"branch=${BRANCH_NAME};\\");"` && sed -i "s/APP_SECRET=.*/APP_SECRET=${APP_SECRET}/g" .env' + sh '''sed -i 's/DATABASE_URL=.*/DATABASE_URL=${DatabaseUrlTesting}/g' .env.test''' + sh 'composer dump-env test' + sh 'php8.0 bin/console doctrine:schema:update --force'; + } + + } + } + + } + } + + stage('Test') { + parallel { + stage('Frontend') { + steps { + dir(path: 'src/frontend') { + sh 'ng lint' + } + + } + } + + stage('Backend') { + steps { + dir(path: 'src/backend') { + sh 'php8.0 vendor/bin/phpunit' + } + + } + } + + } + } + + stage('Configure') { + parallel { + stage('Frontend') { + steps { + dir(path: 'src/frontend') { + sh 'echo skip' + } + + } + } + + stage('Backend') { + steps { + dir(path: 'src/backend') { + sh 'APP_SECRET=`php8.0 -r "echo md5(\\"branch=${BRANCH_NAME};\\");"` && sed -i "s/APP_SECRET=.*/APP_SECRET=${APP_SECRET}/g" .env' + sh 'sed -i "s/DATABASE_URL=.*/DATABASE_URL=${DatabaseUrl}/g" .env.dev' + sh 'composer dump-env dev' + } + + } + } + + } + } + + stage('Build') { + parallel { + stage('Frontend') { + steps { + dir(path: 'src/frontend') { + sh 'ng b -c production' + } + + } + } + + stage('Backend') { + steps { + dir(path: 'src/backend') { + sh 'echo skip' + } + + } + } + + } + } + + stage('Deploy') { + when { + anyOf { + branch 'master' + branch 'staging' + branch 'beta' + branch 'dev' + } + + } + + parallel { + stage('Frontend') { + steps { + dir(path: 'src/frontend') { + sh 'ssh web@fufle.net touch ${ProjectPath}/frontend/REMOVEME' + sh 'ssh web@fufle.net rm -rf ${ProjectPath}/frontend/*' + sh 'scp -r dist/* web@fufle.net:${ProjectPath}/frontend/' + } + + } + } + + stage('Backend') { + steps { + dir(path: 'src/backend') { + sh 'ssh web@fufle.net touch ${ProjectPath}/backend/REMOVEME' + sh 'ssh web@fufle.net rm -rf ${ProjectPath}/backend/*' + sh 'scp -r ./* web@fufle.net:${ProjectPath}/backend/' + } + + } + } + + } + } + + } +} \ No newline at end of file diff --git a/src/backend/.env.test b/src/backend/.env.test index 9e7162f..06142a2 100644 --- a/src/backend/.env.test +++ b/src/backend/.env.test @@ -4,3 +4,4 @@ APP_SECRET='$ecretf0rt3st' SYMFONY_DEPRECATIONS_HELPER=999999 PANTHER_APP_ENV=panther PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots +DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8" diff --git a/src/frontend/src/app/app-routing.module.ts b/src/frontend/src/app/app-routing.module.ts index 8458c35..a221c4b 100644 --- a/src/frontend/src/app/app-routing.module.ts +++ b/src/frontend/src/app/app-routing.module.ts @@ -3,100 +3,111 @@ import { Route, RouterModule } from '@angular/router'; import { NotFoundComponent } from './modules/shared/components/not-found/not-found.component'; interface AppMenuEntry { - label: string; - icon: string; - path?: string; - matchExact?: boolean; - level?: number; + label: string; + icon: string; + path?: string; + matchExact?: boolean; + level?: number; } interface AppRoute extends Route { - menuEntries?: AppMenuEntry[]; + menuEntries?: AppMenuEntry[]; } export const appRoutes: AppRoute[] = [ - { - path: '', - component: NotFoundComponent, - menuEntries: [ - { - label: 'MENU.HOME', - icon: 'fa fa-home', - matchExact: true, - }, - ], - }, - { - path: 'profile', - component: NotFoundComponent, - menuEntries: [ - { - label: 'MENU.PROFILE', - icon: 'fa fa-user', - }, - ], - }, - { - path: 'community', - component: NotFoundComponent, - menuEntries: [ - { - label: 'MENU.COMMUNITY', - icon: 'fa fa-users', - }, - ], - }, - { - path: 'messages', - component: NotFoundComponent, - menuEntries: [ - { - label: 'MENU.MESSAGES', - icon: 'fa fa-comments', - }, - ], - }, - { - path: 'tests', - component: NotFoundComponent, - menuEntries: [ - { - label: 'MENU.TESTS', - icon: 'fa fa-heartbeat', - }, - ], - }, - { - path: 'research', - component: NotFoundComponent, - menuEntries: [ - { - label: 'MENU.RESEARCH', - icon: 'fa fa-flask', - }, - ], - }, - { - path: 'admin', - loadChildren: () => import('./modules/admin/admin.module').then(m => m.AdminModule), - menuEntries: [ - { - label: 'MENU.ADMIN', - icon: 'fa fa-id-card', - matchExact: true, - }, - { - path: 'admin/users', - label: 'MENU.USERS', - icon: 'fa fa-users', - level: 1, - } - ], - }, + { + path: '', + component: NotFoundComponent, + menuEntries: [ + { + label: 'MENU.HOME', + icon: 'fa fa-home', + matchExact: true, + }, + ], + }, + { + path: 'profile', + component: NotFoundComponent, + menuEntries: [ + { + label: 'MENU.PROFILE', + icon: 'fa fa-user', + }, + ], + }, + { + path: 'community', + component: NotFoundComponent, + menuEntries: [ + { + label: 'MENU.COMMUNITY', + icon: 'fa fa-users', + }, + ], + }, + { + path: 'questions', + component: NotFoundComponent, + menuEntries: [ + { + label: 'MENU.QUESTIONS', + icon: 'fa fa-question', + level: 1, + } + ], + }, + { + path: 'messages', + component: NotFoundComponent, + menuEntries: [ + { + label: 'MENU.MESSAGES', + icon: 'fa fa-comments', + }, + ], + }, + { + path: 'tests', + component: NotFoundComponent, + menuEntries: [ + { + label: 'MENU.TESTS', + icon: 'fa fa-heartbeat', + }, + ], + }, + { + path: 'research', + component: NotFoundComponent, + menuEntries: [ + { + label: 'MENU.RESEARCH', + icon: 'fa fa-flask', + }, + ], + }, + { + path: 'admin', + loadChildren: () => import('./modules/admin/admin.module').then(m => m.AdminModule), + menuEntries: [ + { + label: 'MENU.ADMIN', + icon: 'fa fa-id-card', + matchExact: true, + }, + { + path: 'admin/users', + label: 'MENU.USERS', + icon: 'fa fa-users', + level: 1, + } + ], + }, ]; @NgModule({ - imports: [RouterModule.forRoot(appRoutes)], - exports: [RouterModule] + imports: [RouterModule.forRoot(appRoutes)], + exports: [RouterModule] }) export class AppRoutingModule { } diff --git a/src/frontend/src/app/app.component.ts b/src/frontend/src/app/app.component.ts index 446080d..9bbdd01 100644 --- a/src/frontend/src/app/app.component.ts +++ b/src/frontend/src/app/app.component.ts @@ -36,7 +36,7 @@ export class AppComponent { avatar: 'https://www.gravatar.com/avatar/81b206a89f89d5b1123b87606075c6a8?s=514&d=robohash', }; - get isSidebarOpen() { + get isSidebarOpen(): boolean { switch (this.sidebarOpen) { case SidebarOpenEnum.Open: return true; @@ -86,7 +86,7 @@ export class AppComponent { } } - onThemeChanged(theme: string) { + onThemeChanged(theme: string): void { if (theme === undefined) { return; } @@ -95,7 +95,7 @@ export class AppComponent { this.isDark = theme.indexOf('dark') !== -1; } - onResize(size: WindowSize) { + onResize(size: WindowSize): void { if (size === undefined) { return; } @@ -104,7 +104,7 @@ export class AppComponent { this.sidebarMode = this.isMobile ? 'over' : 'side'; } - onLangChanged(lang: string) { + onLangChanged(lang: string): void { if (lang === undefined) { return; } @@ -112,16 +112,16 @@ export class AppComponent { this.langControl.setValue(lang); } - toggleSidebar() { + toggleSidebar(): void { this.sidebarOpen = this.isSidebarOpen ? SidebarOpenEnum.Closed : SidebarOpenEnum.Open; this.browserStorageService.setItem('sidebar.open', this.sidebarOpen); } - onSidebarOpenedChange(opened: boolean) { + onSidebarOpenedChange(opened: boolean): void { this.sidebarOpen = opened ? SidebarOpenEnum.Open : SidebarOpenEnum.Closed; } url(url: string): SafeStyle { return this.sanitizer.bypassSecurityTrustStyle(`url('${url}')`); } -} \ No newline at end of file +} diff --git a/src/frontend/src/app/app.module.ts b/src/frontend/src/app/app.module.ts index 6b83379..aadeb8f 100644 --- a/src/frontend/src/app/app.module.ts +++ b/src/frontend/src/app/app.module.ts @@ -12,7 +12,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; -export function HttpLoaderFactory(http: HttpClient) { +export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { return new TranslateHttpLoader(http, '/assets/lang/', '.json'); } diff --git a/src/frontend/src/app/modules/shared/services/app/app.service.ts b/src/frontend/src/app/modules/shared/services/app/app.service.ts index 40ae29e..5299e2d 100644 --- a/src/frontend/src/app/modules/shared/services/app/app.service.ts +++ b/src/frontend/src/app/modules/shared/services/app/app.service.ts @@ -13,13 +13,14 @@ export class WindowSize { this.isMobile = this.width <= 700; } - update() { - this.width = window.innerWidth; - this.height = window.innerHeight; + static generate(): WindowSize { + return new WindowSize(window.innerWidth, window.innerHeight); } - static generate() { - return new WindowSize(window.innerWidth, window.innerHeight); + update(): WindowSize { + this.width = window.innerWidth; + this.height = window.innerHeight; + return this; } } @@ -41,29 +42,29 @@ export class AppService { }); this.sizeChange.subscribe(size => this.size = size); this.sizeChange.next(WindowSize.generate()); - let lang = browserStorageService.getItem('language'); - if (!lang) { - lang = this.defaultLang; + let language = browserStorageService.getItem('language'); + if (!language) { + language = this.defaultLang; } this.translate.setDefaultLang(this.defaultLang); - this.changeLang(lang); + this.changeLang(language); fromEvent(window, 'resize').subscribe(e => { this.sizeChange.next(WindowSize.generate()); - }) + }); } - changeLang(lang: string) { + changeLang(lang: string): void { if (lang !== this.lang) { this.translate.use(lang); this.langChange.next(lang); } } - getLang() { + getLang(): string { return this.lang; } - getLangs() { + getLangs(): string[] { return [ 'pl', 'en', diff --git a/src/frontend/src/app/modules/shared/services/browser-storage/browser-storage.service.ts b/src/frontend/src/app/modules/shared/services/browser-storage/browser-storage.service.ts index 5ed4972..fad291e 100644 --- a/src/frontend/src/app/modules/shared/services/browser-storage/browser-storage.service.ts +++ b/src/frontend/src/app/modules/shared/services/browser-storage/browser-storage.service.ts @@ -5,12 +5,13 @@ export class BrowserStorageService { private storage: Storage; private namespace: string; + constructor() { this.storage = localStorage; this.namespace = 'default'; } - setNamespace(namespace: string) { + setNamespace(namespace: string): void { this.namespace = namespace; } @@ -18,7 +19,7 @@ export class BrowserStorageService { return namespace ?? this.namespace; } - setItem(key: string, value: any, namespace: string | null = null) { + setItem(key: string, value: any, namespace: string | null = null): void { namespace = this.getNamespace(namespace); const path = `${namespace}.${key}`; this.storage.setItem(path, JSON.stringify(value)); diff --git a/src/frontend/src/app/modules/shared/services/theme/theme.service.ts b/src/frontend/src/app/modules/shared/services/theme/theme.service.ts index 080d21c..637f7d9 100644 --- a/src/frontend/src/app/modules/shared/services/theme/theme.service.ts +++ b/src/frontend/src/app/modules/shared/services/theme/theme.service.ts @@ -21,11 +21,11 @@ export class ThemeService { this.setTheme(userTheme); } - getTheme() { + getTheme(): string { return this.theme; } - setTheme(theme: string) { + setTheme(theme: string): void { if (theme !== this.theme) { this.themeChange.next(theme); document.getElementById('current-theme').setAttribute('href', `/${theme}.css`); @@ -37,5 +37,5 @@ export class ThemeService { 'light', 'dark', ]; - }; + } } diff --git a/src/frontend/src/assets/lang/en.json b/src/frontend/src/assets/lang/en.json index 58dc61a..cb83b1b 100644 --- a/src/frontend/src/assets/lang/en.json +++ b/src/frontend/src/assets/lang/en.json @@ -19,7 +19,9 @@ "HOME": "Dashboard", "PROFILE": "Profile", "COMMUNITY": "Community", + "GROUP": "My group", "MESSAGES": "Messages", + "QUESTIONS": "Q&A", "TESTS": "Laboratory tests", "RESEARCH": "Research", "ADMIN": "Admin", diff --git a/src/frontend/src/assets/lang/pl.json b/src/frontend/src/assets/lang/pl.json index 6855253..64b3e9c 100644 --- a/src/frontend/src/assets/lang/pl.json +++ b/src/frontend/src/assets/lang/pl.json @@ -19,7 +19,9 @@ "HOME": "Kokpit", "PROFILE": "Profil", "COMMUNITY": "Społeczność", + "GROUP": "Moja grupa", "MESSAGES": "Wiadomości", + "QUESTIONS": "Pytania i odpowiedzi", "TESTS": "Wyniki badań", "RESEARCH": "Badania naukowe", "ADMIN": "Panel administratora", diff --git a/src/frontend/src/environments/environment.prod.ts b/src/frontend/src/environments/environment.prod.ts index 3612073..c831413 100644 --- a/src/frontend/src/environments/environment.prod.ts +++ b/src/frontend/src/environments/environment.prod.ts @@ -1,3 +1,5 @@ export const environment = { - production: true + production: true, + buildId: -1001, + buildTime: -1002, }; diff --git a/src/frontend/src/environments/environment.ts b/src/frontend/src/environments/environment.ts index 30d7bcc..50099fc 100644 --- a/src/frontend/src/environments/environment.ts +++ b/src/frontend/src/environments/environment.ts @@ -3,7 +3,9 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, + buildId: -1001, + buildTime: -1002, }; /*