├── web ├── src │ ├── assets │ │ ├── .gitkeep │ │ ├── help │ │ │ ├── add-users.md │ │ │ ├── my-data.md │ │ │ ├── add-repository.md │ │ │ ├── remove-repository.md │ │ │ ├── coming-soon.md │ │ │ ├── open-source.md │ │ │ ├── delete-project.md │ │ │ ├── edit-project.md │ │ │ ├── create-project.md │ │ │ └── quickstart.md │ │ └── scss │ │ │ ├── _variable.scss │ │ │ └── mixin.scss │ ├── app │ │ ├── main │ │ │ ├── main.component.html │ │ │ ├── main.component.scss │ │ │ ├── components │ │ │ │ ├── help-detail │ │ │ │ │ ├── help-detail.component.scss │ │ │ │ │ ├── help-detail.component.html │ │ │ │ │ └── help-detail.component.ts │ │ │ │ ├── legal │ │ │ │ │ ├── privacy │ │ │ │ │ │ ├── privacy.component.ts │ │ │ │ │ │ ├── privacy.component.scss │ │ │ │ │ │ └── privacy.component.html │ │ │ │ │ └── terms-conditions │ │ │ │ │ │ ├── terms-conditions.component.ts │ │ │ │ │ │ ├── terms-conditions.component.scss │ │ │ │ │ │ └── terms-conditions.component.html │ │ │ │ ├── following │ │ │ │ │ ├── following.component.scss │ │ │ │ │ ├── following.component.html │ │ │ │ │ └── following.component.ts │ │ │ │ ├── stats │ │ │ │ │ ├── stats.component.ts │ │ │ │ │ └── stats.component.scss │ │ │ │ ├── help │ │ │ │ │ ├── help.component.scss │ │ │ │ │ ├── help.component.html │ │ │ │ │ └── help.component.ts │ │ │ │ ├── profile │ │ │ │ │ └── profile.component.ts │ │ │ │ └── homepage │ │ │ │ │ └── homepage.component.scss │ │ │ ├── main.component.ts │ │ │ └── main.module.ts │ │ ├── shared │ │ │ ├── models │ │ │ │ ├── breadcrumb.model.ts │ │ │ │ ├── build-times.model.ts │ │ │ │ ├── rating.model.ts │ │ │ │ ├── account.model.ts │ │ │ │ ├── navigation.model.ts │ │ │ │ ├── help-topic.model.ts │ │ │ │ ├── model.model.ts │ │ │ │ ├── contributor.model.ts │ │ │ │ ├── user.model.ts │ │ │ │ ├── activity.model.ts │ │ │ │ ├── status.model.ts │ │ │ │ ├── repositories.model.ts │ │ │ │ ├── loginAudit.model.ts │ │ │ │ ├── release.model.ts │ │ │ │ ├── userStats.model.ts │ │ │ │ ├── milestone.model.ts │ │ │ │ ├── issue.model.ts │ │ │ │ ├── webhook.model.ts │ │ │ │ ├── event.model.ts │ │ │ │ ├── factories │ │ │ │ │ └── model.factory.ts │ │ │ │ ├── ping.model.ts │ │ │ │ ├── access.model.ts │ │ │ │ ├── pullRequest.model.ts │ │ │ │ ├── index.model.ts │ │ │ │ ├── profile.model.ts │ │ │ │ ├── stats.model.ts │ │ │ │ ├── token.model.ts │ │ │ │ └── help.model.ts │ │ │ ├── components │ │ │ │ ├── breadcrumb │ │ │ │ │ ├── breadcrumb.component.scss │ │ │ │ │ ├── breadcrumb.component.html │ │ │ │ │ └── breadcrumb.component.ts │ │ │ │ ├── projects-list │ │ │ │ │ └── projects-list.component.scss │ │ │ │ └── private-public-project │ │ │ │ │ ├── private-public-project.component.ts │ │ │ │ │ ├── private-public-project.component.scss │ │ │ │ │ └── private-public-project.component.html │ │ │ ├── dialog │ │ │ │ ├── confirmation │ │ │ │ │ ├── dialog-confirmation.component.html │ │ │ │ │ └── dialog-confirmation.component.ts │ │ │ │ └── list │ │ │ │ │ ├── dialog-list.component.scss │ │ │ │ │ ├── dialog-list.component.ts │ │ │ │ │ └── dialog-list.component.html │ │ │ ├── directives │ │ │ │ └── markdown.directive.ts │ │ │ └── shared.module.ts │ │ ├── error │ │ │ ├── error.component.scss │ │ │ ├── error.component.html │ │ │ ├── error.component.ts │ │ │ ├── error-routing.module.ts │ │ │ └── error.module.ts │ │ ├── projects │ │ │ ├── list │ │ │ │ ├── list-my-projects.component.html │ │ │ │ └── list-my-projects.component.ts │ │ │ ├── rating │ │ │ │ ├── rating.component.scss │ │ │ │ ├── rating.component.html │ │ │ │ └── rating.component.ts │ │ │ ├── create-edit │ │ │ │ └── create-edit.component.scss │ │ │ ├── projects.module.ts │ │ │ ├── monitor-summary │ │ │ │ └── monitor-summary.component.ts │ │ │ └── projects-routing.module.ts │ │ ├── pipes │ │ │ ├── index.pipe.ts │ │ │ ├── urlencode.pipe.ts │ │ │ ├── type.pipe.ts │ │ │ ├── numbers.pipe.ts │ │ │ ├── state.pipe.ts │ │ │ ├── truncate.pipe.ts │ │ │ ├── user.pipe.ts │ │ │ ├── digits.pipe.ts │ │ │ ├── pipes.module.ts │ │ │ └── time-ago.pipe.ts │ │ ├── core │ │ │ ├── services │ │ │ │ ├── index.service.ts │ │ │ │ ├── spinner.service.ts │ │ │ │ ├── sorting.service.ts │ │ │ │ ├── application.service.ts │ │ │ │ ├── ping.service.ts │ │ │ │ └── activity.service.ts │ │ │ ├── core.module.ts │ │ │ ├── resolvers │ │ │ │ ├── my-projects.resolver.ts │ │ │ │ ├── public-projects.resolver.ts │ │ │ │ ├── tokens.resolver.ts │ │ │ │ ├── repository.resolver.ts │ │ │ │ ├── help.resolver.ts │ │ │ │ ├── edit-token.resolver.ts │ │ │ │ ├── pings.resolver.ts │ │ │ │ ├── view-project.resolver.ts │ │ │ │ └── edit-project.resolver.ts │ │ │ ├── guards │ │ │ │ ├── admin.guard.ts │ │ │ │ └── authentication.guard.ts │ │ │ └── interceptors │ │ │ │ ├── github.http.interceptor.ts │ │ │ │ └── error.http.interceptor.ts │ │ ├── admin │ │ │ ├── admin.module.ts │ │ │ ├── admin-routing.module.ts │ │ │ └── users-list │ │ │ │ ├── users-list.component.ts │ │ │ │ └── users-list.component.scss │ │ ├── tokens │ │ │ ├── tokens-create-edit │ │ │ │ └── tokens-create-edit.component.scss │ │ │ ├── tokens.module.ts │ │ │ ├── tokens-list │ │ │ │ └── tokens-list.component.scss │ │ │ └── tokens-routing.module.ts │ │ ├── app-routing.module.ts │ │ ├── monitors │ │ │ ├── monitors.module.ts │ │ │ ├── pings-list │ │ │ │ └── pings-list.component.scss │ │ │ ├── monitor-create-edit │ │ │ │ └── monitor-create-edit.component.scss │ │ │ ├── monitors-routing.module.ts │ │ │ └── monitors-list │ │ │ │ └── monitors-list.component.scss │ │ ├── app.module.ts │ │ └── app-material.module.ts │ ├── environments │ │ ├── config.model.ts │ │ ├── environment.ts │ │ └── environment.prod.ts │ ├── main.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── test.ts ├── test │ ├── support │ │ ├── commands.js │ │ └── index.js │ ├── integration │ │ ├── navigation │ │ │ └── navigation.feature │ │ ├── help │ │ │ └── quickstart.feature │ │ └── homepage │ │ │ └── homepage.feature │ ├── step_definitions │ │ ├── action.js │ │ ├── data.js │ │ └── browser.js │ └── plugins │ │ └── index.js ├── README.md ├── .editorconfig ├── cypress.json ├── tsconfig.json └── .gitignore ├── functions ├── README.md ├── src │ ├── models │ │ ├── user.model.ts │ │ ├── index.model.ts │ │ ├── project.model.ts │ │ ├── monitor.model.ts │ │ └── ping.model.ts │ ├── mappers │ │ └── github │ │ │ ├── webhook-event-response │ │ │ ├── shared │ │ │ │ ├── index.ts │ │ │ │ ├── interfaces.ts │ │ │ │ ├── milestone.ts │ │ │ │ ├── user.ts │ │ │ │ ├── functions.ts │ │ │ │ ├── issue.ts │ │ │ │ └── repository.ts │ │ │ ├── index.ts │ │ │ ├── member.ts │ │ │ ├── watch.ts │ │ │ ├── create.ts │ │ │ ├── issue-comment.ts │ │ │ └── status.ts │ │ │ ├── index.mapper.ts │ │ │ ├── organisation.mapper.ts │ │ │ ├── contributor.mapper.ts │ │ │ ├── status.mapper.ts │ │ │ ├── user.mapper.ts │ │ │ ├── milestone.mapper.ts │ │ │ ├── issue.mapper.ts │ │ │ ├── payload.mapper.ts │ │ │ ├── release.mapper.ts │ │ │ └── event.mapper.ts │ ├── client │ │ ├── ping.ts │ │ ├── logger.ts │ │ ├── github.ts │ │ └── firebase-admin.ts │ ├── repository │ │ ├── find-git-webhook-repository.ts │ │ └── update-repository.ts │ ├── project │ │ └── project.ts │ ├── user │ │ ├── events.ts │ │ ├── stats.ts │ │ ├── repos.ts │ │ └── create-user.ts │ ├── shared │ │ └── delete-collection.ts │ ├── environments │ │ └── environment.ts │ └── scheduler │ │ └── schedule.ts ├── .gitignore ├── tsconfig.json └── package.json ├── .gitignore ├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── documentation.md │ ├── feature_request.md │ └── bug_report.md └── CONTRIBUTING.md ├── firestore-debug.log ├── firebase.dev.json.enc ├── firebase.dev.json.gpg ├── firebase.json ├── firestore.indexes.json └── firestore.rules /web/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/test/support/commands.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/help/add-users.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/help/my-data.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/help/add-repository.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/assets/help/remove-repository.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functions/README.md: -------------------------------------------------------------------------------- 1 | # Firebase functions 2 | -------------------------------------------------------------------------------- /web/test/support/index.js: -------------------------------------------------------------------------------- 1 | import './commands'; 2 | -------------------------------------------------------------------------------- /web/src/app/main/main.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .firebase 4 | *.log 5 | *.local.* 6 | *.enc.* 7 | -------------------------------------------------------------------------------- /web/src/app/main/main.component.scss: -------------------------------------------------------------------------------- 1 | .margin-top-10 { 2 | margin-top: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /web/src/assets/help/coming-soon.md: -------------------------------------------------------------------------------- 1 | ### Coming soon 2 | 3 | This page is coming soon :) 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [eddiejaoude] 4 | -------------------------------------------------------------------------------- /firestore-debug.log: -------------------------------------------------------------------------------- 1 | Dev App Server is now running. 2 | 3 | API endpoint: http://localhost:8080 4 | -------------------------------------------------------------------------------- /firebase.dev.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DashboardHub/PipelineDashboard/HEAD/firebase.dev.json.enc -------------------------------------------------------------------------------- /firebase.dev.json.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DashboardHub/PipelineDashboard/HEAD/firebase.dev.json.gpg -------------------------------------------------------------------------------- /web/src/app/shared/models/breadcrumb.model.ts: -------------------------------------------------------------------------------- 1 | export class BreadCrumbModel { 2 | link: string; 3 | title: string; 4 | } 5 | -------------------------------------------------------------------------------- /web/src/app/shared/models/build-times.model.ts: -------------------------------------------------------------------------------- 1 | export class BuildTimes { 2 | context: string; 3 | time: number; 4 | } 5 | -------------------------------------------------------------------------------- /web/src/app/shared/models/rating.model.ts: -------------------------------------------------------------------------------- 1 | export class RatingModel { 2 | name: string; 3 | description: string; 4 | value: number; 5 | } 6 | -------------------------------------------------------------------------------- /web/src/app/error/error.component.scss: -------------------------------------------------------------------------------- 1 | div:first-child { 2 | height: 100vh; 3 | 4 | img { 5 | max-height: 40%; 6 | max-width: 40%; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /functions/src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | export class UserModel { 2 | username: string; 3 | avatarUrl: string; 4 | oauth: { 5 | githubToken: string; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /web/src/app/shared/models/account.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Account model 3 | */ 4 | export class Account { 5 | type: 'free' | 'admin' = 'free'; 6 | private: number = 0; 7 | } 8 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Typescript v1 declaration files 6 | typings/ 7 | 8 | node_modules/ 9 | *.log 10 | -------------------------------------------------------------------------------- /functions/src/models/index.model.ts: -------------------------------------------------------------------------------- 1 | export * from './monitor.model'; 2 | export * from './ping.model'; 3 | export * from './project.model'; 4 | export * from './repository.model'; 5 | -------------------------------------------------------------------------------- /web/src/app/shared/models/navigation.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Navigation model 3 | */ 4 | export class Navigation { 5 | title: string; 6 | route: string; 7 | icon: string; 8 | } 9 | -------------------------------------------------------------------------------- /web/src/app/projects/list/list-my-projects.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/src/app/shared/models/help-topic.model.ts: -------------------------------------------------------------------------------- 1 | export class HelpTopic { 2 | title: string; 3 | description: string; 4 | path: string; 5 | updatedAt: string; 6 | icon?: string; 7 | content?: string; 8 | } 9 | -------------------------------------------------------------------------------- /web/src/app/shared/models/model.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Model interface 3 | */ 4 | export interface IModel { } 5 | 6 | /** 7 | * Model model 8 | */ 9 | export abstract class Model { 10 | abstract toData(): T1; 11 | } 12 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './repository'; 2 | export * from './user'; 3 | export * from './milestone'; 4 | export * from './functions'; 5 | export * from './issue'; 6 | export * from './interfaces'; 7 | -------------------------------------------------------------------------------- /web/src/app/shared/models/contributor.model.ts: -------------------------------------------------------------------------------- 1 | // Application model 2 | import { UserModel } from './user.model'; 3 | 4 | /** 5 | * Contribution model 6 | */ 7 | export class ContributorModel { 8 | owner: UserModel; 9 | total: number; 10 | } 11 | -------------------------------------------------------------------------------- /web/src/assets/help/open-source.md: -------------------------------------------------------------------------------- 1 | Open source is social coding! DashboardHub is all completely open and transparent, with not only the whole codebase available but also our project board and meeting notes. Driven by the community, you influence the features you want! 2 | -------------------------------------------------------------------------------- /web/src/app/projects/rating/rating.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/scss/mixin.scss"; 2 | 3 | .rating-container { 4 | @include pageContainer; 5 | 6 | &__header { 7 | @include pageHeader; 8 | } 9 | 10 | &__card { 11 | margin-top: 20px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web/src/app/main/components/help-detail/help-detail.component.scss: -------------------------------------------------------------------------------- 1 | .help-detail { 2 | margin: 20px; 3 | padding: 20px; 4 | img { 5 | max-width: 100%; 6 | } 7 | pre { 8 | overflow-x: auto; 9 | white-space: pre-wrap; 10 | word-wrap: break-word; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /web/src/app/shared/models/user.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * User model 3 | */ 4 | export class UserModel { 5 | uid: string = ''; 6 | name: string; 7 | username: string; 8 | avatarUrl: string = ''; 9 | url: string = ''; 10 | lastSignedIn: Date; 11 | creationTime: Date; 12 | } 13 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/shared/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { GitHubEventModel } from "../../event.mapper"; 2 | import { Repository } from "./repository"; 3 | 4 | export interface HubEventActions { 5 | convertToHubEvent(): GitHubEventModel; 6 | repository: Repository; 7 | } 8 | -------------------------------------------------------------------------------- /web/src/app/pipes/index.pipe.ts: -------------------------------------------------------------------------------- 1 | export * from './digits.pipe'; 2 | export * from './numbers.pipe'; 3 | export * from './state.pipe'; 4 | export * from './time-ago.pipe'; 5 | export * from './truncate.pipe'; 6 | export * from './type.pipe'; 7 | export * from './urlencode.pipe'; 8 | export * from './user.pipe'; 9 | -------------------------------------------------------------------------------- /web/src/app/shared/models/activity.model.ts: -------------------------------------------------------------------------------- 1 | import { IRepository } from './repository.model'; 2 | 3 | /** 4 | * Activity model 5 | */ 6 | export class ActivityModel { 7 | type: string; 8 | payload: { title: string, action?: string }; 9 | repository: IRepository; 10 | createdOn: Date; 11 | } 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | closes #{issue_number} 2 | 3 | ### Notes 4 | 5 | A summary of what was achieved in this PR 6 | 7 | - [ ] API: Task One (eg. updated `deployed` model ) 8 | - [ ] UI: Task Three (eg. added validation rules on create environment page) 9 | - [ ] Documentation: Updated accordingly 10 | -------------------------------------------------------------------------------- /web/src/app/error/error.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Error 404 Page not found

5 |

Please go back to the homepage and start again

6 |
7 |
8 | -------------------------------------------------------------------------------- /web/src/app/main/main.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component } from '@angular/core'; 3 | 4 | /** 5 | * Main component 6 | */ 7 | @Component({ 8 | selector: 'dashboard-main', 9 | templateUrl: './main.component.html', 10 | styleUrls: ['./main.component.scss'], 11 | }) 12 | export class MainComponent { } 13 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Web 2 | 3 | UI part of the app. 4 | 5 | ## Quickstart 6 | 7 | ### Requirements 8 | 9 | You should have done these aready to be here: 10 | 11 | 1. clone whole project 12 | 2. `cd web/` 13 | 14 | ### Running web locally 15 | 16 | 1. `npm install` 17 | 2. `npm start` 18 | 3. navigate to http://localhost:4200 19 | -------------------------------------------------------------------------------- /web/src/app/error/error.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component } from '@angular/core'; 3 | 4 | /** 5 | * Error component 6 | */ 7 | @Component({ 8 | selector: 'dashboard-error', 9 | templateUrl: './error.component.html', 10 | styleUrls: ['./error.component.scss'], 11 | }) 12 | export class ErrorComponent {} 13 | -------------------------------------------------------------------------------- /web/src/app/pipes/urlencode.pipe.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | 4 | /** 5 | * Encode uri pipe 6 | */ 7 | @Pipe({ name: 'encodeUri' }) 8 | export class UrlencodePipe implements PipeTransform { 9 | transform(uri: string): string { 10 | return encodeURI(uri); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6"], 4 | "module": "commonjs", 5 | "noImplicitReturns": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "target": "es6" 9 | }, 10 | "compileOnSave": true, 11 | "include": [ 12 | "src" 13 | ], 14 | "exclude": [ 15 | "**/.history/**" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /web/src/app/shared/models/status.model.ts: -------------------------------------------------------------------------------- 1 | // Firestore module 2 | 3 | export type state = 'open' | 'closed' | 'failure' | 'success' | 'pending'; 4 | 5 | /** 6 | * Pull Request status model 7 | */ 8 | export class PullRequestStatusModel { 9 | id: number; 10 | state: state; 11 | context: string; 12 | createdAt: string; 13 | updatedAt: string; 14 | } 15 | -------------------------------------------------------------------------------- /web/src/app/shared/models/repositories.model.ts: -------------------------------------------------------------------------------- 1 | // Firestore module 2 | import { firestore } from 'firebase'; 3 | 4 | // Application model 5 | import { RepositoryModel } from './repository.model'; 6 | 7 | /** 8 | * Repository model 9 | */ 10 | export class RepositoriesModel { 11 | lastUpdated: firestore.Timestamp; 12 | data: RepositoryModel[] = []; 13 | } 14 | -------------------------------------------------------------------------------- /web/src/app/shared/components/breadcrumb/breadcrumb.component.scss: -------------------------------------------------------------------------------- 1 | div { 2 | font-size: 20px; 3 | .avatar { 4 | width: 40px; 5 | border-radius: 50%; 6 | object-fit: cover; 7 | height: 40px; 8 | cursor: pointer; 9 | 10 | img { 11 | width: 32px; 12 | } 13 | 14 | .mat-icon { 15 | font-size: 35px; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/src/app/shared/models/loginAudit.model.ts: -------------------------------------------------------------------------------- 1 | // Firestore module 2 | import { firestore } from 'firebase'; 3 | 4 | /** 5 | * LoginAudit model 6 | */ 7 | export class LoginAuditModel { 8 | date: firestore.Timestamp; 9 | userAgent: string; 10 | device: string; 11 | os: string; 12 | osVersion: string; 13 | browser: string; 14 | browserVersion: string; 15 | } 16 | -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*.*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | max_line_length = off 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.feature] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /web/src/app/main/components/legal/privacy/privacy.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component } from '@angular/core'; 3 | 4 | /** 5 | * Legal privacy component 6 | */ 7 | @Component({ 8 | selector: 'dashboard-legal-privacy', 9 | templateUrl: './privacy.component.html', 10 | styleUrls: ['./privacy.component.scss'], 11 | }) 12 | export class PrivacyComponent { 13 | } 14 | -------------------------------------------------------------------------------- /web/src/environments/config.model.ts: -------------------------------------------------------------------------------- 1 | export class Config { 2 | production: boolean; 3 | version: string; 4 | 5 | firebase: { 6 | apiKey: string, 7 | authDomain: string, 8 | databaseURL: string, 9 | projectId: string, 10 | storageBucket: string, 11 | messagingSenderId: string, 12 | appId?: string, 13 | }; 14 | 15 | tracking: string; 16 | } 17 | -------------------------------------------------------------------------------- /functions/src/models/project.model.ts: -------------------------------------------------------------------------------- 1 | import { MonitorModel } from './monitor.model'; 2 | 3 | /** 4 | * Project Model for functions 5 | */ 6 | export class ProjectModel { 7 | 8 | monitors?: MonitorModel[] = []; 9 | uid?: string = ''; 10 | url?: string = ''; 11 | repositories?: string[] = []; 12 | 13 | constructor(uid: string = '') { 14 | this.uid = uid; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /web/src/app/core/services/index.service.ts: -------------------------------------------------------------------------------- 1 | export * from './activity.service'; 2 | export * from './authentication.service'; 3 | export * from './monitor.service'; 4 | export * from './ping.service'; 5 | export * from './project.service'; 6 | export * from './token.service'; 7 | export * from './repository.service'; 8 | export * from './sorting.service'; 9 | export * from './user.service'; 10 | -------------------------------------------------------------------------------- /web/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './config.model'; 2 | 3 | export const environment: Config = { 4 | production: false, 5 | version: 'x.x.x', 6 | 7 | firebase: { 8 | apiKey: '', 9 | authDomain: '', 10 | databaseURL: '', 11 | projectId: '', 12 | storageBucket: '', 13 | messagingSenderId: '', 14 | }, 15 | 16 | tracking: '', 17 | }; 18 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/index.ts: -------------------------------------------------------------------------------- 1 | export * from './issues'; 2 | export * from './milestone'; 3 | export * from './pull-request'; 4 | export * from './release'; 5 | export * from './repository'; 6 | export * from './watch'; 7 | export * from './create'; 8 | export * from './push'; 9 | export * from './issue-comment'; 10 | export * from './member'; 11 | export * from './status'; 12 | -------------------------------------------------------------------------------- /web/src/app/shared/models/release.model.ts: -------------------------------------------------------------------------------- 1 | // Firestore module 2 | import { firestore } from 'firebase'; 3 | 4 | // User model 5 | import { UserModel } from './user.model'; 6 | 7 | /** 8 | * Release model 9 | */ 10 | export class ReleaseModel { 11 | uid: string; 12 | title: string; 13 | description: string; 14 | owner: UserModel; 15 | htmlUrl: string; 16 | createdOn: firestore.Timestamp; 17 | } 18 | -------------------------------------------------------------------------------- /web/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:4200", 3 | "ignoreTestFiles": "*.js", 4 | "fixturesFolder": "test/fixtures", 5 | "integrationFolder": "test/integration", 6 | "pluginsFile": "test/plugins", 7 | "screenshotsFolder": "test/screenshots", 8 | "supportFile": "test/support/index.js", 9 | "videosFolder": "test/videos", 10 | "viewportWidth": 1920, 11 | "viewportHeight": 1080 12 | } 13 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | import 'hammerjs'; 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | platformBrowserDynamic().bootstrapModule(AppModule); 14 | -------------------------------------------------------------------------------- /functions/src/client/ping.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'request-promise-native'; 2 | 3 | export interface PingResponse { 4 | statusCode: number; 5 | body: string; 6 | } 7 | 8 | export async function Ping(uri: string): Promise { 9 | return await get({ 10 | uri: uri, 11 | headers: { 12 | 'User-Agent': 'DashboardHub', 13 | }, 14 | json: true, 15 | resolveWithFullResponse: true, 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /web/src/assets/help/delete-project.md: -------------------------------------------------------------------------------- 1 | # How do I delete an environment I have created? 2 | 1) Click into the environment 3 | 4 | ![Click Into Environment](https://user-images.githubusercontent.com/21239137/40559018-3f323a62-604d-11e8-8a8f-4aad87ae3360.png) 5 | 6 | 2) Then, click the bin icon to delete the environment 7 | 8 | ![Delete Environment](https://user-images.githubusercontent.com/21239137/40559662-6d91de2e-604f-11e8-8e36-7c621e333ca6.png) 9 | -------------------------------------------------------------------------------- /web/src/app/main/components/legal/terms-conditions/terms-conditions.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component } from '@angular/core'; 3 | 4 | /** 5 | * Legal terms and condition component 6 | */ 7 | @Component({ 8 | selector: 'dashboard-legal-terms-conditions', 9 | templateUrl: './terms-conditions.component.html', 10 | styleUrls: ['./terms-conditions.component.scss'], 11 | }) 12 | export class TermsConditionsComponent { 13 | } 14 | -------------------------------------------------------------------------------- /web/src/app/shared/models/userStats.model.ts: -------------------------------------------------------------------------------- 1 | // Application model 2 | import { ActivityModel } from './activity.model'; 3 | 4 | /** 5 | * User stats model 6 | */ 7 | export class UserStatsModel { 8 | name: string; 9 | username: string; 10 | avatarUrl: string = ''; 11 | github: { 12 | repository: { 13 | total: number, 14 | }, 15 | activity: { 16 | latest: ActivityModel, 17 | }, 18 | }; 19 | lastUpdated: Date; 20 | } 21 | -------------------------------------------------------------------------------- /functions/src/client/logger.ts: -------------------------------------------------------------------------------- 1 | import { LoggingWinston } from '@google-cloud/logging-winston'; 2 | import * as winston from 'winston'; 3 | 4 | export const Logger: winston.Logger = winston.createLogger({ 5 | // format: winston.format.combine( 6 | // winston.format.timestamp(), 7 | // winston.format.json() 8 | // ), 9 | level: 'info', 10 | transports: [ 11 | new winston.transports.Console(), 12 | new LoggingWinston(), 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /functions/src/mappers/github/index.mapper.ts: -------------------------------------------------------------------------------- 1 | export * from './contributor.mapper'; 2 | export * from './event.mapper'; 3 | export * from './issue.mapper'; 4 | export * from './milestone.mapper'; 5 | export * from './organisation.mapper'; 6 | export * from './payload.mapper'; 7 | export * from './pullRequest.mapper'; 8 | export * from './release.mapper'; 9 | export * from './repository.mapper'; 10 | export * from './user.mapper'; 11 | export * from './webhook.mapper'; 12 | -------------------------------------------------------------------------------- /functions/src/models/monitor.model.ts: -------------------------------------------------------------------------------- 1 | import { PingModel } from './ping.model'; 2 | 3 | export type MonitorMethod = 'GET' | 'POST'; 4 | 5 | /** 6 | * Monitor model for functions 7 | */ 8 | export class MonitorModel { 9 | expectedCode: number; 10 | expectedText?: string; 11 | method: MonitorMethod; 12 | name: string; 13 | path: string; 14 | uid: string; 15 | latestPing: PingModel; 16 | successfulPings: number; 17 | unsuccessfulPings: number; 18 | } 19 | -------------------------------------------------------------------------------- /web/src/app/pipes/type.pipe.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | 4 | export enum TypeData { 5 | build = 'Build', 6 | deploy = 'Deploy', 7 | 'build-deploy' = 'Build & Deploy', 8 | } 9 | 10 | /** 11 | * Environment type pipe 12 | */ 13 | @Pipe({ name: 'environmentType' }) 14 | export class EnvironmentTypePipe implements PipeTransform { 15 | transform(type: string): string { 16 | return TypeData[type]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/src/app/shared/dialog/confirmation/dialog-confirmation.component.html: -------------------------------------------------------------------------------- 1 |

{{ data.title }}

2 |
{{ data.content }}
3 |
4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /web/src/app/main/components/help-detail/help-detail.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ data.icon }} {{ data.title }} 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /web/src/app/shared/models/milestone.model.ts: -------------------------------------------------------------------------------- 1 | // Firestore model 2 | import { firestore } from 'firebase'; 3 | 4 | // DashboardHub model 5 | import { UserModel } from './user.model'; 6 | 7 | /** 8 | * Milestone model 9 | */ 10 | export class MilestoneModel { 11 | title: string; 12 | creator: UserModel; 13 | state: string; 14 | openIssues: number; 15 | closeIssues: number; 16 | htmlUrl: string; 17 | description: string; 18 | updatedAt: firestore.Timestamp; 19 | } 20 | -------------------------------------------------------------------------------- /web/src/assets/help/edit-project.md: -------------------------------------------------------------------------------- 1 | # How do I edit an environment that I have created? 2 | 1) Once you have created an environment, click into the environment 3 | 4 | ![Click Into Environment](https://user-images.githubusercontent.com/21239137/40559018-3f323a62-604d-11e8-8a8f-4aad87ae3360.png) 5 | 6 | 2) Then, click the pen icon on the top right to edit the environment 7 | 8 | ![Edit Environment](https://user-images.githubusercontent.com/21239137/40558607-b96ed54e-604b-11e8-85a7-384a567b6dd4.png) 9 | -------------------------------------------------------------------------------- /functions/src/mappers/github/organisation.mapper.ts: -------------------------------------------------------------------------------- 1 | export interface GitHubOrganisationtInput { 2 | login: string; 3 | avatar_url: string; 4 | } 5 | 6 | export interface GitHubOrganisationModel { 7 | username: string; 8 | avatarUrl: string; 9 | } 10 | export class GitHubOrganisationMapper { 11 | static import(input: GitHubOrganisationtInput): GitHubOrganisationModel { 12 | return { 13 | username: input.login, 14 | avatarUrl: input.avatar_url, 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Suggest an improvement to our documentation 4 | title: '' 5 | labels: 'stack: docs' 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Documentation Feedback 11 | 12 | Provide a brief summary of what you would like to see changed in our 13 | documentation 14 | 15 | Feel free to provide any suggestions of content or examples you’d like us to include. 16 | 17 | **Affected documentation page:** Insert a link to the affected page 18 | -------------------------------------------------------------------------------- /web/src/app/pipes/numbers.pipe.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | 4 | /** 5 | * Number formatter pipe 6 | */ 7 | @Pipe({ name: 'numbers' }) 8 | export class NumbersPipe implements PipeTransform { 9 | transform(digits: number, precision: number = 1): string { 10 | if (digits > 1000) { 11 | return Number.isInteger(digits / 100) ? digits / 1000 + 'k' : (digits / 1000).toFixed(1) + 'k'; 12 | } 13 | 14 | return digits.toString(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /web/src/app/error/error-routing.module.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { NgModule } from '@angular/core'; 3 | 4 | // Dashboard hub core modules 5 | import { RouterModule, Routes } from '@angular/router'; 6 | import { ErrorComponent } from './error.component'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: '', 11 | component: ErrorComponent, 12 | }, 13 | ]; 14 | 15 | @NgModule({ 16 | imports: [RouterModule.forChild(routes)], 17 | exports: [RouterModule], 18 | }) 19 | export class ErrorRoutingModule { } 20 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/shared/milestone.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user'; 2 | 3 | export interface Milestone { 4 | url: string; 5 | html_url: string; 6 | labels_url: string; 7 | id: number; 8 | node_id: string; 9 | number: number; 10 | title: string; 11 | description: string; 12 | creator: User; 13 | open_issues: number; 14 | closed_issues: number; 15 | state: string; 16 | created_at: string; 17 | updated_at: string; 18 | due_on: string; 19 | closed_at?: string; 20 | } 21 | -------------------------------------------------------------------------------- /web/src/app/shared/models/issue.model.ts: -------------------------------------------------------------------------------- 1 | // Firestore module 2 | import { firestore } from 'firebase'; 3 | 4 | // Application model 5 | import { UserModel } from './user.model'; 6 | 7 | /** 8 | * Issue model 9 | */ 10 | export class IssueModel { 11 | uid: string = ''; 12 | url: string = ''; 13 | state: ''; 14 | title: string = ''; 15 | number: number; 16 | owner: UserModel; 17 | assigned: UserModel; 18 | description: string = ''; 19 | createdOn: firestore.Timestamp; 20 | updatedOn: firestore.Timestamp; 21 | } 22 | -------------------------------------------------------------------------------- /functions/src/models/ping.model.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from "firebase-admin"; 2 | 3 | export type PingType = 'scheduler' | 'manual'| 'automatic'; 4 | /** 5 | * Pings model for functions 6 | */ 7 | export interface PingModel { 8 | uid: string; 9 | monitorUid: string; 10 | codeMatched: boolean; 11 | duration: number; 12 | expectedCode: number; 13 | expectedText: string; 14 | url: string; 15 | isValid?: boolean; 16 | statusCode: number; 17 | textMatched: boolean; 18 | type: PingType; 19 | createdOn: firestore.Timestamp; 20 | } 21 | -------------------------------------------------------------------------------- /web/src/app/error/error.module.ts: -------------------------------------------------------------------------------- 1 | // Core components 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | // Application component 6 | import { SharedModule } from '@shared/shared.module'; 7 | import { ErrorRoutingModule } from './error-routing.module'; 8 | import { ErrorComponent } from './error.component'; 9 | 10 | @NgModule({ 11 | declarations: [ErrorComponent], 12 | imports: [ 13 | CommonModule, 14 | ErrorRoutingModule, 15 | SharedModule, 16 | ], 17 | }) 18 | export class ErrorModule { } 19 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/shared/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | login: string; 3 | id: number; 4 | node_id: string; 5 | avatar_url: string; 6 | gravatar_id: string; 7 | url: string; 8 | html_url: string; 9 | followers_url: string; 10 | following_url: string; 11 | gists_url: string; 12 | starred_url: string; 13 | subscriptions_url: string; 14 | organizations_url: string; 15 | repos_url: string; 16 | events_url: string; 17 | received_events_url: string; 18 | type: string; 19 | site_admin: boolean; 20 | } 21 | -------------------------------------------------------------------------------- /web/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './config.model'; 2 | 3 | export const environment: Config = { 4 | production: true, 5 | version: 'x.x.x', 6 | 7 | firebase: { 8 | apiKey: '{{ FIREBASE_API_KEY }}', 9 | authDomain: '{{ FIREBASE_AUTH_DOMAIN }}', 10 | databaseURL: '{{ FIREBASE_DATABASE_URL }}', 11 | projectId: '{{ FIREBASE_PROJECT_ID }}', 12 | storageBucket: '{{ FIREBASE_STORAGE_BUCKET }}', 13 | messagingSenderId: '{{ FIREBASE_MESSAGING_SEND_ID }}', 14 | }, 15 | 16 | tracking: 'UA-60804146-5', 17 | }; 18 | -------------------------------------------------------------------------------- /web/test/integration/navigation/navigation.feature: -------------------------------------------------------------------------------- 1 | Feature: Main Navigation 2 | 3 | Scenario: Click links in the main navigation 4 | Given the "/" page is open 5 | When click on list item ".sidenav .menu__item" at position 0 6 | Then the text "Homepage" is in the element ".home__title" 7 | When click on list item ".sidenav .menu__item" at position 1 8 | Then the text "Features" is in the element ".feature__title" 9 | When click on list item ".sidenav .menu__item" at position 2 10 | Then the text "Help" is in the element ".help__title" 11 | -------------------------------------------------------------------------------- /web/src/app/shared/models/webhook.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webhook model 3 | */ 4 | export class WebhookModel { 5 | type: string; 6 | id: number; 7 | name: string; 8 | active: boolean; 9 | events: string[]; 10 | config: { 11 | url: string; 12 | contentType?: 'form' | 'json'; 13 | secret?: string; 14 | insecureSsl?: '0' | '1'; 15 | }; 16 | updatedAt: Date; 17 | createdAt: Date; 18 | url: string; 19 | testUrl: string; 20 | pingUrl: string; 21 | lastResponse: { 22 | code?: any; 23 | status: string; 24 | message?: any; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /web/src/app/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | // DashboardHub modules and components 6 | import { SharedModule } from '@shared/shared.module'; 7 | import { AdminRoutingModule } from './admin-routing.module'; 8 | import { UsersListComponent } from './users-list/users-list.component'; 9 | 10 | @NgModule({ 11 | declarations: [UsersListComponent], 12 | imports: [ 13 | CommonModule, 14 | SharedModule, 15 | AdminRoutingModule, 16 | ], 17 | }) 18 | export class AdminModule { } 19 | -------------------------------------------------------------------------------- /web/src/app/pipes/state.pipe.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | 4 | export enum StateData { 5 | startBuild = 'Build Started', 6 | finishBuild = 'Build Finished', 7 | failBuild = 'Build Failed', 8 | startDeploy = 'Deploy Started', 9 | finishDeploy = 'Deploy Finished', 10 | failDeploy = 'Deployed Failed', 11 | } 12 | 13 | /** 14 | * State Pipe 15 | */ 16 | @Pipe({ name: 'releaseState' }) 17 | export class ReleaseStatePipe implements PipeTransform { 18 | transform(state: string): string { 19 | return StateData[state]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "functions": { 7 | "predeploy": [ 8 | "npm --prefix \"$RESOURCE_DIR\" run lint", 9 | "npm --prefix \"$RESOURCE_DIR\" run build" 10 | ] 11 | }, 12 | "hosting": { 13 | "public": "web/dist", 14 | "ignore": [ 15 | "firebase.json", 16 | "**/.*", 17 | "**/node_modules/**" 18 | ], 19 | "rewrites": [ 20 | { 21 | "source": "**", 22 | "destination": "/index.html" 23 | } 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /functions/src/mappers/github/contributor.mapper.ts: -------------------------------------------------------------------------------- 1 | import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './index.mapper'; 2 | 3 | export interface GitHubContributorInput { 4 | author: GitHubUserInput; 5 | total: number; 6 | } 7 | 8 | export interface GitHubContributorModel { 9 | owner: GitHubUserModel; 10 | total: number; 11 | } 12 | 13 | export class GitHubContributorMapper { 14 | static import(input: GitHubContributorInput): GitHubContributorModel { 15 | return { 16 | owner: GitHubUserMapper.import(input.author), 17 | total: input.total, 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /web/test/step_definitions/action.js: -------------------------------------------------------------------------------- 1 | import { Then, When } from 'cypress-cucumber-preprocessor/steps'; 2 | 3 | When(/^the "([^"]*)" link at position (\d+) is clicked$/, (link, position) => { 4 | cy.get(link).eq(position).click(); 5 | }); 6 | 7 | When(/^enter text "([^"]*)" in the element "([^"]*)"$/, (text, element) => { 8 | cy.get(element).type(text); 9 | }); 10 | 11 | Then(/^clear text in the element "([^"]*)"$/, (element) => { 12 | cy.get(element).clear(); 13 | }); 14 | 15 | When(/^click on list item "([^"]*)" at position (\d+)$/, (item, position) => { 16 | cy.get(item).eq(position).click(); 17 | }); 18 | -------------------------------------------------------------------------------- /web/src/app/shared/models/event.model.ts: -------------------------------------------------------------------------------- 1 | // Firestore module 2 | import { firestore } from 'firebase'; 3 | 4 | // Application model 5 | import { RepositoryModel } from './repository.model'; 6 | import { UserModel } from './user.model'; 7 | 8 | /** 9 | * Event model 10 | */ 11 | export class EventModel { 12 | uid: string; 13 | type: string; 14 | public: string; 15 | actor: UserModel; 16 | repository: RepositoryModel; 17 | // organisation: input.org ? GitHubOrganisationMapper.import(input.org) : {}, 18 | // payload: GitHubPayloadMapper.import(input.type, input.payload), 19 | createdOn: firestore.Timestamp; 20 | } 21 | -------------------------------------------------------------------------------- /web/src/app/admin/admin-routing.module.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | 5 | // DashboardHub components 6 | import { AdminGuard } from '@app/core/guards/admin.guard'; 7 | import { UsersListComponent } from './users-list/users-list.component'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: 'users', 12 | component: UsersListComponent, 13 | canActivate: [AdminGuard], 14 | }, 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forChild(routes)], 19 | exports: [RouterModule], 20 | }) 21 | export class AdminRoutingModule { } 22 | -------------------------------------------------------------------------------- /web/src/app/shared/models/factories/model.factory.ts: -------------------------------------------------------------------------------- 1 | // Application model 2 | import { IModel, Model } from '../model.model'; 3 | 4 | /** 5 | * Manage to and from data / models 6 | */ 7 | export class ModelFactory { 8 | static toModels>(data: T1[], obj: new (a: IModel) => T2): T2[] { 9 | return data.map((item: T1): T2 => new obj(item)); 10 | } 11 | 12 | /** 13 | * Method to convert models to interface 14 | * @param models Model 15 | */ 16 | static fromModels, T2 extends IModel>(models: T1[]): T2[] { 17 | return models.map((model: T1): T2 => model.toData()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/src/app/pipes/truncate.pipe.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | 4 | /** 5 | * Truncate pipe 6 | */ 7 | @Pipe({ name: 'truncate' }) 8 | export class TruncatePipe implements PipeTransform { 9 | transform(text: any, length: number): string { 10 | if (typeof text !== 'string') { 11 | return ''; 12 | } 13 | 14 | let truncated: string = text.substr(0, length); 15 | 16 | if (text.length > length) { 17 | if (truncated.lastIndexOf(' ') > 0) { 18 | truncated = truncated.trim(); 19 | } 20 | 21 | truncated += '…'; 22 | } 23 | 24 | return truncated; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /web/src/app/main/components/following/following.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/scss/variable'; 2 | @import '../../../../assets/scss/mixin.scss'; 3 | 4 | .project { 5 | padding: 20px 30px; 6 | 7 | .project-card { 8 | @include cardContainer; 9 | margin: 30px 0; 10 | 11 | &__header { 12 | @include header-icon; 13 | background-color: $blue-2-color; 14 | box-shadow: 0px 3px 6px $blue-2-shadow; 15 | } 16 | 17 | &__title { 18 | @include title; 19 | 20 | &__subtitle { 21 | @include subtitle; 22 | } 23 | } 24 | 25 | &__content { 26 | padding: 0; 27 | @include tableContainer; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /web/src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | // Dashboard hub interceptors 6 | import { ErrorHttpInterceptorModule } from '@core/interceptors/error.http.interceptor'; 7 | import { GitHubHttpInterceptorModule } from '@core/interceptors/github.http.interceptor'; 8 | 9 | // Dashboard hub material module 10 | import { AppMaterialModule } from '@app/app-material.module'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, AppMaterialModule, GitHubHttpInterceptorModule, ErrorHttpInterceptorModule, 15 | ], 16 | declarations: [], 17 | providers: [], 18 | }) 19 | export class CoreModule { 20 | } 21 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/shared/functions.ts: -------------------------------------------------------------------------------- 1 | import { DocumentData } from "../../../../client/firebase-admin"; 2 | import { HubEventActions } from "./interfaces"; 3 | 4 | export function isExistProperties(obj: any, keys: string[]){ 5 | if (!obj) { 6 | return false; 7 | } 8 | 9 | for (const key of keys) { 10 | if (!obj.hasOwnProperty(key)) { 11 | return false; 12 | } 13 | } 14 | 15 | return true; 16 | } 17 | 18 | export function addHubEventToCollection(repository: DocumentData, event: HubEventActions) { 19 | if (!Array.isArray(repository.events)) { 20 | repository.events = []; 21 | } 22 | 23 | repository.events.unshift(event.convertToHubEvent()); 24 | } 25 | -------------------------------------------------------------------------------- /web/src/app/pipes/user.pipe.ts: -------------------------------------------------------------------------------- 1 | // 3rd party imports 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | 5 | // DashboardHub imports 6 | import { UserService } from '@core/services/index.service'; 7 | import { UserModel } from '@shared/models/index.model'; 8 | 9 | /** 10 | * User pipe 11 | */ 12 | @Pipe({ name: 'user' }) 13 | export class UserPipe implements PipeTransform { 14 | 15 | /** 16 | * Life cycle method 17 | * @param userService UserService 18 | */ 19 | constructor(private userService: UserService) { } 20 | 21 | transform(userId: string): Observable { 22 | return this.userService 23 | .findUserStatsById(userId); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/src/app/shared/components/projects-list/projects-list.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../assets/scss/mixin.scss"; 2 | 3 | .project { 4 | 5 | &__list { 6 | width: 100%; 7 | 8 | img { 9 | width: 24px; 10 | } 11 | 12 | &__title { 13 | word-break: break-all; 14 | 15 | @media(max-width: $breakpoint-xs) { 16 | 17 | width: 30vw; 18 | } 19 | 20 | 21 | } 22 | 23 | &__actions { 24 | cursor: pointer; 25 | } 26 | 27 | &__url { 28 | max-width: 20vw; 29 | 30 | &__content { 31 | @include ellipsisText; 32 | width: 90%; 33 | } 34 | } 35 | 36 | &__monitors { 37 | &__icon { 38 | padding-top: 6px; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /web/src/app/tokens/tokens-create-edit/tokens-create-edit.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../styles.scss"; 2 | @import "../../../assets/scss/variable"; 3 | 4 | 5 | .token-settings { 6 | @include pageContainer; 7 | 8 | &__header { 9 | @include pageHeader; 10 | } 11 | 12 | &__card { 13 | @include cardContainer; 14 | 15 | &__header { 16 | @include header-icon; 17 | background-color: $blue-2-color; 18 | box-shadow: 0px 3px 6px $blue-2-shadow; 19 | } 20 | 21 | &__title { 22 | @include title; 23 | 24 | &__subtitle { 25 | @include subtitle; 26 | } 27 | } 28 | 29 | &__content { 30 | padding-top: 40px; 31 | display: block; 32 | overflow: hidden; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /web/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "declaration": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": ["es2016", "dom"], 9 | "module": "es2015", 10 | "moduleResolution": "node", 11 | "outDir": "../dist/out-tsc", 12 | "sourceMap": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "../node_modules/@types" 16 | ], 17 | "types": [ 18 | "hammerjs" 19 | ], 20 | "noUnusedParameters": false, 21 | "noUnusedLocals": false, 22 | "allowUnreachableCode": false, 23 | "pretty": true 24 | }, 25 | "exclude": [ 26 | "test.ts", 27 | "**/*.spec.*" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /web/src/app/pipes/digits.pipe.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | 4 | /** 5 | * Digits pipe 6 | */ 7 | @Pipe({ name: 'digits' }) 8 | export class DigitsPipe implements PipeTransform { 9 | transform(digits: number, precision: number = 1): string { 10 | if (digits === 0) { 11 | return '0'; 12 | } 13 | 14 | if (!isFinite(digits)) { 15 | return '-'; 16 | } 17 | 18 | const units: string[] = ['', 'K', 'M', 'B', 'T']; 19 | const value: number = Math.floor(Math.log(digits) / Math.log(1000)); 20 | 21 | if (value === 0) { 22 | precision = 0; 23 | } 24 | 25 | return ((digits / Math.pow(1000, Math.floor(value))).toFixed(precision) + ' ' + units[value]).trim(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "declaration": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": ["es6", "dom"], 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "outDir": "../dist/out-tsc", 12 | "sourceMap": true, 13 | "target": "es6", 14 | "typeRoots": [ 15 | "../node_modules/@types" 16 | ], 17 | "types": [ 18 | "jasmine", "hammerjs", "node" 19 | ], 20 | "noUnusedParameters": false, 21 | "noUnusedLocals": false, 22 | "allowUnreachableCode": false, 23 | "pretty": true 24 | }, 25 | "include": [ 26 | "test.ts", 27 | "**/*.spec.ts" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /web/src/app/main/components/following/following.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | dashboard 6 |
7 | 8 | {{ title }} 9 | 10 | You are following {{ projects.length }} projects 11 |
12 | 13 | 14 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /web/src/app/shared/dialog/confirmation/dialog-confirmation.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component, Inject } from '@angular/core'; 3 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; 4 | 5 | /** 6 | * Confirmation dialog 7 | */ 8 | @Component({ 9 | selector: 'dashboard-dialog-confirmation', 10 | templateUrl: './dialog-confirmation.component.html', 11 | }) 12 | export class DialogConfirmationComponent { 13 | 14 | /** 15 | * Life cycle method 16 | * @param dialogRef MatDialogRef 17 | * @param data data to be send in dialog 18 | */ 19 | constructor( 20 | public dialogRef: MatDialogRef, 21 | @Inject(MAT_DIALOG_DATA) public data: { title: string, content?: string } 22 | ) { } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /web/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { NgModule } from '@angular/core'; 3 | 4 | // Dashboard hub core modules 5 | import { RouterModule, Routes } from '@angular/router'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | loadChildren: './main/main.module#MainModule', 11 | }, 12 | { 13 | path: 'projects', 14 | loadChildren: './projects/projects.module#ProjectsModule', 15 | }, 16 | { 17 | path: 'admin', 18 | loadChildren: './admin/admin.module#AdminModule', 19 | }, 20 | { 21 | path: '**', 22 | loadChildren: './error/error.module#ErrorModule', 23 | }, 24 | ]; 25 | 26 | @NgModule({ 27 | imports: [RouterModule.forRoot(routes)], 28 | exports: [RouterModule], 29 | }) 30 | export class AppRoutingModule { } 31 | -------------------------------------------------------------------------------- /web/src/app/core/services/spinner.service.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Injectable } from '@angular/core'; 3 | import { Observable, Subject } from 'rxjs'; 4 | 5 | /** 6 | * Spinner service 7 | */ 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class SpinnerService { 12 | 13 | private spinnerSubject: Subject = new Subject(); 14 | 15 | /** 16 | * Set the progress bar status 17 | * @param status status of the progress bar 18 | */ 19 | public setProgressBar(status: boolean): void { 20 | this.spinnerSubject.next(status); 21 | } 22 | 23 | /** 24 | * Returns the status of progress bar to main component 25 | */ 26 | public getProgressBar(): Observable { 27 | return this.spinnerSubject.asObservable(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/src/app/pipes/pipes.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { 4 | DigitsPipe, 5 | EnvironmentTypePipe, 6 | NumbersPipe, 7 | ReleaseStatePipe, 8 | TimeAgoPipe, 9 | TruncatePipe, 10 | UrlencodePipe, 11 | UserPipe 12 | } from './index.pipe'; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | DigitsPipe, 17 | EnvironmentTypePipe, 18 | NumbersPipe, 19 | ReleaseStatePipe, 20 | TimeAgoPipe, 21 | TruncatePipe, 22 | UrlencodePipe, 23 | UserPipe, 24 | ], 25 | exports: [ 26 | DigitsPipe, 27 | EnvironmentTypePipe, 28 | NumbersPipe, 29 | ReleaseStatePipe, 30 | TimeAgoPipe, 31 | TruncatePipe, 32 | UrlencodePipe, 33 | UserPipe, 34 | ], 35 | }) 36 | export class PipesModule { 37 | } 38 | -------------------------------------------------------------------------------- /web/src/app/shared/models/ping.model.ts: -------------------------------------------------------------------------------- 1 | export type PingType = 'scheduler' | 'manual'| 'automatic'; 2 | 3 | /** 4 | * Pings model for web 5 | */ 6 | 7 | export interface IPing { 8 | uid: string; 9 | codeMatched: boolean; 10 | duration: number; 11 | expectedCode: number; 12 | expectedText: string; 13 | url: string; 14 | isValid?: boolean; 15 | statusCode: number; 16 | textMatched: boolean; 17 | type: PingType; 18 | createdOn: Date; 19 | } 20 | 21 | export class PingModel { 22 | uid: string; 23 | codeMatched: boolean; 24 | duration: number; 25 | expectedCode: number; 26 | expectedText: string; 27 | url: string; 28 | isValid?: boolean; 29 | statusCode: number; 30 | textMatched: boolean; 31 | type: PingType; 32 | createdOn: Date; 33 | } 34 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "importHelpers": true, 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "noUnusedLocals": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2017", 18 | "dom" 19 | ], 20 | "module": "es2015", 21 | "baseUrl": "./", 22 | "paths": { 23 | "@app/*": ["src/app/*"], 24 | "@shared/*": ["src/app/shared/*"], 25 | "@core/*": ["src/app/core/*"] 26 | } 27 | }, 28 | "exclude": [ 29 | "**/.history/**" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /web/src/app/tokens/tokens.module.ts: -------------------------------------------------------------------------------- 1 | // Angular modules 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | // Dashboard hub routing modules 6 | import { SharedModule } from '@shared/shared.module'; 7 | import { TokensRoutingModule } from './tokens-routing.module'; 8 | 9 | // Dashboard hub components 10 | import { TokensCreateEditComponent } from './tokens-create-edit/tokens-create-edit.component'; 11 | import { TokensListComponent } from './tokens-list/tokens-list.component'; 12 | 13 | @NgModule({ 14 | imports: [ 15 | CommonModule, 16 | TokensRoutingModule, 17 | SharedModule, 18 | ], 19 | declarations: [ 20 | TokensCreateEditComponent, 21 | TokensListComponent, 22 | ], 23 | }) 24 | export class TokensModule { } 25 | -------------------------------------------------------------------------------- /web/src/app/core/services/sorting.service.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Injectable } from '@angular/core'; 3 | 4 | /** 5 | * Sorting service 6 | */ 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class SortingService { 11 | /** 12 | * Sort the list by date field 13 | */ 14 | sortListByDate(list: T[], field: string): void { 15 | list.sort((obj1: any, obj2: any) => { 16 | const date1: Date = new Date(obj1[field]); 17 | const date2: Date = new Date(obj2[field]); 18 | 19 | return date1 > date2 ? -1 : date1 < date2 ? 1 : 0; 20 | }); 21 | } 22 | 23 | /** 24 | * Sort the list by any key 25 | */ 26 | sortListByNumber(list: T[], field: string): void { 27 | list.sort((obj1: any, obj2: any) => { 28 | return obj2[field] - obj1[field]; 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /functions/src/mappers/github/status.mapper.ts: -------------------------------------------------------------------------------- 1 | export interface BuildTimes { 2 | context: string; 3 | time: number; 4 | } 5 | 6 | export interface GitHubPullRequestStatusInput { 7 | id: number; 8 | state: string; 9 | context: string; 10 | created_at: string; 11 | updated_at: string; 12 | } 13 | 14 | export interface GitHubPullRequestStatusModel { 15 | id: number; 16 | state: string; 17 | context: string; 18 | createdAt: string; 19 | updatedAt: string; 20 | } 21 | 22 | export class GitHubPullRequestStatusMapper { 23 | static import(input: GitHubPullRequestStatusInput): GitHubPullRequestStatusModel { 24 | return { 25 | id: input.id, 26 | state: input.state, 27 | context: input.context, 28 | createdAt: input.created_at, 29 | updatedAt: input.updated_at, 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /deploy 6 | /tmp 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | 19 | # IDE - VSCode 20 | .vscode/* 21 | .vscode/settings.json 22 | .vscode/tasks.json 23 | .vscode/launch.json 24 | .vscode/extensions.json 25 | 26 | # misc 27 | /.sass-cache 28 | /connect.lock 29 | /coverage/* 30 | /libpeerconnection.log 31 | npm-debug.log 32 | testem.log 33 | /typings 34 | /.vagrant 35 | yarn-error.log 36 | 37 | # e2e 38 | cypress/screenshots 39 | cypress/videos 40 | 41 | #System Files 42 | .DS_Store 43 | Thumbs.db 44 | 45 | 46 | #cypress files 47 | test/videos 48 | test/screenshots 49 | 50 | #documentation 51 | documentation 52 | -------------------------------------------------------------------------------- /web/src/app/monitors/monitors.module.ts: -------------------------------------------------------------------------------- 1 | // Core components 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | // Dashboard hub monitor module and components 6 | import { SharedModule } from '@shared/shared.module'; 7 | import { MonitorCreateEditComponent } from './monitor-create-edit/monitor-create-edit.component'; 8 | import { MonitorsListComponent } from './monitors-list/monitors-list.component'; 9 | import { MonitorsRoutingModule } from './monitors-routing.module'; 10 | import { PingsListComponent } from './pings-list/pings-list.component'; 11 | 12 | @NgModule({ 13 | declarations: [MonitorsListComponent, MonitorCreateEditComponent, PingsListComponent], 14 | imports: [ 15 | CommonModule, 16 | MonitorsRoutingModule, 17 | SharedModule, 18 | ], 19 | }) 20 | export class MonitorsModule { } 21 | -------------------------------------------------------------------------------- /web/test/step_definitions/data.js: -------------------------------------------------------------------------------- 1 | Given(/^there is a document "([^"]*)" with the field "([^"]*)" set to (\d+) in collection "([^"]*)"$/, (id, field, value, collection) => { 2 | cy.task('db:update', { collection, id, field, value }) 3 | }); 4 | 5 | Then(/^the count "([^"]*)" is in the element "([^"]*)"$/, (count, field) => { 6 | cy.get(field).contains(count); 7 | }); 8 | 9 | Then(/^total count of element "([^"]*)" is (\d+)$/, (field, count) => { 10 | cy.get(field).should('have.length', count); 11 | }); 12 | 13 | Given(/^there is the following document in the collection "([^"]*)":$/, (collection, table) => { 14 | const data = {}; 15 | table.hashes().forEach((item) => { 16 | try { 17 | data[item.field] = JSON.parse(item.value); 18 | } catch(e) { console.log(e) } 19 | }); 20 | cy.task('db:save', { collection, uid: data.uid, data }); 21 | }); 22 | -------------------------------------------------------------------------------- /functions/src/repository/find-git-webhook-repository.ts: -------------------------------------------------------------------------------- 1 | import { enviroment } from '../environments/environment'; 2 | import { GitHubRepositoryWebhookResponse } from '../mappers/github/webhook.mapper'; 3 | import { GitHubClient } from './../client/github'; 4 | 5 | 6 | export function listWebhook(repositoryFullName: string, token: string): Promise { 7 | return GitHubClient(`/repos/${repositoryFullName}/hooks`, token); 8 | } 9 | 10 | 11 | export async function findWebhook(repositoryFullName: string, token: string): Promise { 12 | const list: GitHubRepositoryWebhookResponse[] = await listWebhook(repositoryFullName, token); 13 | 14 | return list.find((elem: GitHubRepositoryWebhookResponse) => elem && elem.config && elem.config.url === enviroment.githubWebhook.url) 15 | } 16 | -------------------------------------------------------------------------------- /web/src/app/shared/components/private-public-project/private-public-project.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; 3 | 4 | // Dashboard hub model and services 5 | import { IProject } from '../../models/index.model'; 6 | 7 | /** 8 | * Private public project component 9 | */ 10 | @Component({ 11 | selector: 'dashboard-projects-private-public', 12 | templateUrl: './private-public-project.component.html', 13 | styleUrls: ['./private-public-project.component.scss'], 14 | }) 15 | export class PrivatePublicProjectComponent implements OnChanges { 16 | 17 | @Input() projects: IProject[] = []; 18 | @Input() title: string; 19 | @Input() showTitle: boolean = false; 20 | 21 | ngOnChanges(changes: SimpleChanges): void { 22 | if (changes.projects) { 23 | this.projects = changes.projects.currentValue; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/shared/issue.ts: -------------------------------------------------------------------------------- 1 | import { Milestone } from './milestone'; 2 | import { User } from './user'; 3 | 4 | interface Label { 5 | id: number; 6 | node_id: string; 7 | url: string; 8 | name: string; 9 | color: string; 10 | default: boolean; 11 | } 12 | 13 | 14 | export interface Issue { 15 | url: string; 16 | repository_url: string; 17 | labels_url: string; 18 | comments_url: string; 19 | events_url: string; 20 | html_url: string; 21 | id: number; 22 | node_id: string; 23 | number: number; 24 | title: string; 25 | user: User; 26 | labels: Label[]; 27 | state: string; 28 | locked: boolean; 29 | assignee: User; 30 | assignees: User[]; 31 | milestone: Milestone; 32 | comments: number; 33 | created_at: string; 34 | updated_at: string; 35 | closed_at?: any; 36 | author_association: string; 37 | body: string; 38 | } 39 | -------------------------------------------------------------------------------- /web/src/app/shared/models/access.model.ts: -------------------------------------------------------------------------------- 1 | // Applicatoin model 2 | import { IModel, Model } from './model.model'; 3 | 4 | /** 5 | * Access interface 6 | */ 7 | export interface IAccess extends IModel { 8 | admin: string[]; 9 | readonly?: string[]; 10 | } 11 | 12 | /** 13 | * Access model 14 | */ 15 | export class AccessModel extends Model implements IAccess { 16 | admin: string[]; 17 | readonly?: string[]; 18 | 19 | /** 20 | * Life cycle method 21 | * @param access IAccess 22 | */ 23 | constructor(access?: IAccess) { 24 | super(); 25 | this.admin = access && access.admin ? access.admin : []; 26 | this.readonly = access && access.readonly ? access.readonly : []; 27 | } 28 | 29 | /** 30 | * The models to data only 31 | */ 32 | public toData(): IAccess { 33 | return { 34 | admin: this.admin, 35 | readonly: this.readonly, 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/src/app/shared/models/pullRequest.model.ts: -------------------------------------------------------------------------------- 1 | // Firestore module 2 | import { firestore } from 'firebase'; 3 | 4 | // Application model 5 | import { BuildTimes } from './build-times.model'; 6 | import { state } from './status.model'; 7 | import { UserModel } from './user.model'; 8 | 9 | /** 10 | * PullRequest model 11 | */ 12 | export class PullRequestModel { 13 | uid: string = ''; 14 | url: string = ''; 15 | state: state; 16 | title: string = ''; 17 | owner: UserModel; 18 | id: number; 19 | assigned: UserModel; 20 | requestedReviewers: UserModel; 21 | description: string = ''; 22 | statusesUrl: string = ''; 23 | buildTimes?: BuildTimes[]; 24 | createdOn: firestore.Timestamp; 25 | updatedOn: firestore.Timestamp; 26 | comments: number; 27 | reviewComments: number; 28 | maintainerCanModify: boolean; 29 | commits: number; 30 | additions: number; 31 | deletions: number; 32 | changedFiles: number; 33 | } 34 | -------------------------------------------------------------------------------- /web/test/step_definitions/browser.js: -------------------------------------------------------------------------------- 1 | import { 2 | Given, 3 | Then 4 | } from 'cypress-cucumber-preprocessor/steps'; 5 | 6 | const url = Cypress.config('baseUrl'); 7 | 8 | Given(/^the "([^"]*)" page is open$/, (path) => { 9 | cy.visit(`${url}/${path}`); 10 | }); 11 | 12 | Then(/^the title on the page says "([^"]*)"$/, (text) => { 13 | cy.title().should('include', text); 14 | }); 15 | 16 | Then(/^the text "([^"]*)" is in the element "([^"]*)"$/, (text, element) => { 17 | cy.get(element).contains(text); 18 | }); 19 | 20 | When(/^find the row of text "([^"]*)"$/, (text) => { 21 | cy.contains('tr', text).as('row'); 22 | }); 23 | 24 | Then(/^the text "([^"]*)" is in the row and column "([^"]*)"$/, (text, column) => { 25 | cy.get('@row').get(column).contains(text); 26 | }); 27 | 28 | Then(/^the text "([^"]*)" is not in the element "([^"]*)"$/, (text, element) => { 29 | cy.get(element).contains(text).should('not.exist') 30 | }); 31 | -------------------------------------------------------------------------------- /web/src/app/shared/components/private-public-project/private-public-project.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../assets/scss/variable"; 2 | @import "../../../../assets/scss/mixin.scss"; 3 | 4 | .project { 5 | @include pageContainer; 6 | 7 | &__title { 8 | @include pageHeader; 9 | 10 | &__button { 11 | background-color: $blue-2-color; 12 | color: #fff; 13 | } 14 | 15 | &__header { 16 | font-size: 16pt 17 | } 18 | } 19 | 20 | .project-card { 21 | @include cardContainer; 22 | margin: 30px 0; 23 | 24 | &__header { 25 | @include header-icon; 26 | background-color: $blue-2-color; 27 | box-shadow: 0px 3px 6px $blue-2-shadow; 28 | } 29 | 30 | &__title { 31 | @include title; 32 | 33 | &__subtitle { 34 | @include subtitle; 35 | } 36 | } 37 | 38 | &__content { 39 | padding: 0; 40 | @include tableContainer; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /functions/src/mappers/github/user.mapper.ts: -------------------------------------------------------------------------------- 1 | // Third party modules 2 | import { firestore } from 'firebase-admin'; 3 | 4 | // Dashboard hub firebase functions models 5 | import { GitHubEventModel } from './event.mapper'; 6 | 7 | export interface GitHubUserInput { 8 | login: string; 9 | avatar_url: string; 10 | } 11 | 12 | export interface GitHubUserModel { 13 | username: string; 14 | avatarUrl: string; 15 | } 16 | 17 | export interface GitHubUserStatsModel { 18 | name?: string; 19 | username: string; 20 | avatarUrl: string; 21 | github: { 22 | repository: { 23 | total: number; 24 | }, 25 | activity: { 26 | latest: GitHubEventModel; 27 | } 28 | }; 29 | lastUpdated: firestore.Timestamp; 30 | } 31 | 32 | export class GitHubUserMapper { 33 | static import(input: GitHubUserInput): GitHubUserModel { 34 | return { 35 | username: input.login, 36 | avatarUrl: input.avatar_url, 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /functions/src/project/project.ts: -------------------------------------------------------------------------------- 1 | // 3rd party 2 | import { firestore } from 'firebase-admin'; 3 | import { FirebaseAdmin, WriteResult } from '../client/firebase-admin'; 4 | 5 | export interface ProjectInput { 6 | projectUid: string; 7 | increase?: boolean; 8 | } 9 | 10 | export const updateViews: any = async (projectUid: string): Promise => { 11 | return FirebaseAdmin 12 | .firestore() 13 | .collection('projects') 14 | .doc(projectUid) 15 | .set( 16 | { 17 | views: firestore.FieldValue.increment(1), 18 | }, 19 | { merge: true }); 20 | } 21 | 22 | export const updateFollowers: any = async (projectUid: string, increase: boolean): Promise => { 23 | return FirebaseAdmin 24 | .firestore() 25 | .collection('projects') 26 | .doc(projectUid) 27 | .set( 28 | { 29 | followers: increase ? firestore.FieldValue.increment(1) : firestore.FieldValue.increment(-1), 30 | }, 31 | { merge: true }); 32 | } 33 | -------------------------------------------------------------------------------- /web/src/app/main/components/stats/stats.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component, OnInit } from '@angular/core'; 3 | 4 | // Application model and services 5 | import { ApplicationService } from '@core/services/application.service'; 6 | import { StatsModel } from '@shared/models/stats.model'; 7 | 8 | /** 9 | * Application stats component on dashboard 10 | */ 11 | @Component({ 12 | selector: 'dashboard-stats', 13 | templateUrl: './stats.component.html', 14 | styleUrls: ['./stats.component.scss'], 15 | }) 16 | export class StatsComponent implements OnInit { 17 | 18 | public stats: StatsModel; 19 | 20 | /** 21 | * Life cycle method 22 | * @param applicationService application service 23 | */ 24 | constructor(private applicationService: ApplicationService) { } 25 | 26 | /** 27 | * Life cycle init method 28 | */ 29 | ngOnInit(): void { 30 | this.applicationService.getApplicationStats().subscribe((stats: StatsModel) => this.stats = stats); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /web/src/app/projects/list/list-my-projects.component.ts: -------------------------------------------------------------------------------- 1 | // Core components 2 | import { Component, OnInit } from '@angular/core'; 3 | 4 | // Application services/model 5 | import { ActivatedRoute } from '@angular/router'; 6 | import { IProject, ProjectModel } from '@shared/models/index.model'; 7 | 8 | /** 9 | * List my project component 10 | */ 11 | @Component({ 12 | selector: 'dashboard-list-my-projects', 13 | templateUrl: './list-my-projects.component.html', 14 | }) 15 | export class ListMyProjectsComponent implements OnInit { 16 | 17 | public projects: IProject[] = []; 18 | public title: string = 'My Projects'; 19 | 20 | /** 21 | * Life cycle method 22 | * @param projectService Project service 23 | */ 24 | constructor( 25 | private route: ActivatedRoute 26 | ) { } 27 | 28 | /** 29 | * Life cycle init method 30 | */ 31 | ngOnInit(): void { 32 | this.route.data.subscribe((data: { projects: ProjectModel[] }) => this.projects = data.projects); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /web/src/app/monitors/pings-list/pings-list.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/scss/variable"; 2 | @import "../../../assets/scss/mixin.scss"; 3 | 4 | .monitor-ping { 5 | @include pageContainer; 6 | 7 | &__header { 8 | @include pageHeader; 9 | } 10 | 11 | &__card { 12 | @include cardContainer; 13 | 14 | &__header { 15 | @include header-icon; 16 | background-color: $purple-color; 17 | box-shadow: 0px 3px 6px $purple-shadow; 18 | } 19 | 20 | &__title { 21 | @include title; 22 | 23 | &__subtitle { 24 | @include subtitle; 25 | } 26 | } 27 | 28 | &__content { 29 | padding: 0; 30 | display: block; 31 | overflow: auto; 32 | max-height: 60vh; 33 | 34 | @include tableContainer; 35 | 36 | &__table { 37 | width: 100%; 38 | padding: 0 2px; 39 | 40 | &__url { 41 | width: 30vw; 42 | word-break: break-all; 43 | padding-right: 5px; 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /web/src/app/projects/create-edit/create-edit.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/scss/variable"; 2 | @import "../../../assets/scss/mixin.scss"; 3 | 4 | .create-project { 5 | @include pageContainer; 6 | 7 | &__title { 8 | @include pageHeader; 9 | 10 | } 11 | 12 | &__card { 13 | max-width: 510px; 14 | @include cardContainer; 15 | margin-left: auto; 16 | margin-right: auto; 17 | 18 | &__header { 19 | @include header-icon; 20 | background-color: $blue-2-color; 21 | box-shadow: 0px 3px 6px $blue-2-shadow; 22 | } 23 | 24 | &__title { 25 | @include title; 26 | 27 | &__subtitle { 28 | @include subtitle; 29 | } 30 | } 31 | 32 | &__content { 33 | &__type { 34 | padding-top: 20px; 35 | 36 | &__title { 37 | color: $dark-grey-2-color; 38 | } 39 | 40 | @media (max-width: $breakpoint-xs) { 41 | &__title { 42 | display: none; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /functions/src/client/github.ts: -------------------------------------------------------------------------------- 1 | import { delete as del, get, post } from 'request-promise-native'; 2 | 3 | export async function GitHubClient(uri: string, token: string): Promise { 4 | return await get({ 5 | uri: `https://api.github.com${uri}`, 6 | headers: { 7 | 'User-Agent': 'DashboardHub', 8 | 'Authorization': `token ${token}`, 9 | }, 10 | json: true, 11 | }); 12 | }; 13 | 14 | export async function GitHubClientPost(uri: string, token: string, body: any): Promise { 15 | return await post({ 16 | uri: `https://api.github.com${uri}`, 17 | headers: { 18 | 'User-Agent': 'DashboardHub', 19 | 'Authorization': `token ${token}`, 20 | }, 21 | json: true, 22 | body: body, 23 | }); 24 | }; 25 | 26 | export async function GitHubClientDelete(uri: string, token: string): Promise { 27 | return await del({ 28 | uri: `https://api.github.com${uri}`, 29 | headers: { 30 | 'User-Agent': 'DashboardHub', 31 | 'Authorization': `token ${token}`, 32 | }, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /web/src/app/monitors/monitor-create-edit/monitor-create-edit.component.scss: -------------------------------------------------------------------------------- 1 | 2 | @import "../../../assets/scss/variable"; 3 | @import "../../../assets/scss/mixin.scss"; 4 | .monitor-settings { 5 | @include pageContainer; 6 | 7 | &__header { 8 | @include pageHeader; 9 | } 10 | 11 | &__card { 12 | @include cardContainer; 13 | 14 | &__header { 15 | @include header-icon; 16 | background-color: $blue-2-color; 17 | box-shadow: 0px 3px 6px $blue-2-shadow; 18 | } 19 | 20 | &__title { 21 | @include title; 22 | 23 | &__subtitle { 24 | @include subtitle; 25 | } 26 | } 27 | 28 | &__content { 29 | padding-top: 40px; 30 | display: block; 31 | overflow: hidden; 32 | 33 | &__table { 34 | width: 100%; 35 | 36 | &__avatar { 37 | height: 35px; 38 | border-radius: 50%; 39 | object-fit: cover; 40 | } 41 | 42 | &__ping { 43 | @include ellipsisText; 44 | max-width: 15vw; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /web/src/app/core/resolvers/my-projects.resolver.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Injectable } from '@angular/core'; 3 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; 4 | 5 | // Third party modules 6 | import { Observable } from 'rxjs'; 7 | import { take } from 'rxjs/operators'; 8 | 9 | // Dashboard hub model and services 10 | import { ProjectService } from '@core/services/index.service'; 11 | import { IProject } from '@shared/models/index.model'; 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class MyProjectsResolver implements Resolve { 17 | 18 | /** 19 | * Life cycle method 20 | * @param projectService ProjectService 21 | */ 22 | constructor( 23 | private projectService: ProjectService 24 | ) { } 25 | 26 | /** 27 | * Find all project data before showing projects page 28 | * @param route ActivatedRouteSnapshot 29 | */ 30 | resolve(route: ActivatedRouteSnapshot): Observable { 31 | return this.projectService.findMyProjects() 32 | .pipe( 33 | take(1) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/src/app/shared/dialog/list/dialog-list.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../assets/scss/variable"; 2 | @import "../../../../assets/scss/mixin.scss"; 3 | 4 | .dialog { 5 | 6 | &__header { 7 | &__title { 8 | font-size: 22px; 9 | } 10 | 11 | &__icon { 12 | transform: scale(1.5); 13 | } 14 | 15 | &__update { 16 | font-size: 14px; 17 | font-weight: lighter; 18 | color: $dark-grey-color; 19 | } 20 | } 21 | 22 | &__content { 23 | padding: 20px 20px; 24 | height: 40vh; 25 | 26 | &__name { 27 | font-size: 20px; 28 | @include ellipsisText; 29 | max-width: 60vw; 30 | font-weight: 300; 31 | } 32 | 33 | &__description { 34 | @include ellipsisText; 35 | max-width: 60vw; 36 | font-size: 14px; 37 | font-weight: lighter; 38 | color: $dark-grey-color; 39 | } 40 | 41 | @media (max-width: $breakpoint-xs) { 42 | 43 | &__name { 44 | width: 35vw; 45 | } 46 | 47 | &__description { 48 | width: 35vw; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /web/src/app/core/guards/admin.guard.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Injectable } from '@angular/core'; 3 | import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; 4 | 5 | // Dashboard hub services 6 | import { AuthenticationService } from '@core/services/index.service'; 7 | 8 | /** 9 | * Admin Guard 10 | */ 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class AdminGuard implements CanActivate { 15 | 16 | /** 17 | * Life cycle method 18 | * @param router Router 19 | * @param authService AuthService 20 | */ 21 | constructor(private router: Router, private authService: AuthenticationService) { 22 | } 23 | 24 | /** 25 | * Protect routes for authenticated user 26 | * @param next ActivatedRouteSnapshot 27 | * @param state RouterStateSnapshot 28 | */ 29 | public canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { 30 | if (this.authService.profile.isAdmin) { 31 | return true; 32 | } 33 | 34 | this.router.navigate(['/']); 35 | 36 | return false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/src/app/core/resolvers/public-projects.resolver.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Injectable } from '@angular/core'; 3 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; 4 | 5 | // Third party modules 6 | import { Observable } from 'rxjs'; 7 | import { take } from 'rxjs/operators'; 8 | 9 | // Dashboard hub model and services 10 | import { ProjectService } from '@core/services/index.service'; 11 | import { IProject } from '@shared/models/index.model'; 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class PublicProjectResolver implements Resolve { 17 | 18 | /** 19 | * Life cycle method 20 | * @param projectService ProjectService 21 | */ 22 | constructor( 23 | private projectService: ProjectService 24 | ) { } 25 | 26 | /** 27 | * Find all project data before showing projects page 28 | * @param route ActivatedRouteSnapshot 29 | */ 30 | resolve(route: ActivatedRouteSnapshot): Observable { 31 | return this.projectService.findPublicProjects() 32 | .pipe( 33 | take(1) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/src/app/core/guards/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Injectable } from '@angular/core'; 3 | import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; 4 | 5 | // Dashboard hub services 6 | import { AuthenticationService } from '@core/services/index.service'; 7 | 8 | /** 9 | * Authentication Guard 10 | */ 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class AuthGuard implements CanActivate { 15 | 16 | /** 17 | * Life cycle method 18 | * @param router Router 19 | * @param authService AuthService 20 | */ 21 | constructor(private router: Router, private authService: AuthenticationService) { 22 | } 23 | 24 | /** 25 | * Protect routes for authenticated user 26 | * @param next ActivatedRouteSnapshot 27 | * @param state RouterStateSnapshot 28 | */ 29 | public canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { 30 | if (this.authService.isAuthenticated) { 31 | return true; 32 | } 33 | 34 | this.router.navigate(['/']); 35 | 36 | return false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase serve --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "10" 14 | }, 15 | "main": "lib/index.js", 16 | "dependencies": { 17 | "@google-cloud/logging-winston": "^1.0.0", 18 | "cors": "^2.8.5", 19 | "firebase": "^7.3.0", 20 | "firebase-admin": "^8.2.0", 21 | "firebase-functions": "^3.1.0", 22 | "request": "^2.88.0", 23 | "request-promise-native": "^1.0.7", 24 | "uuid": "^3.3.3", 25 | "winston": "^3.2.1" 26 | }, 27 | "devDependencies": { 28 | "@types/cors": "^2.8.5", 29 | "@types/request-promise-native": "^1.0.16", 30 | "@types/uuid": "3.4.3", 31 | "firebase-functions-test": "^0.1.6", 32 | "tslint": "~5.16.0", 33 | "typescript": "^3.4.5" 34 | }, 35 | "private": true 36 | } 37 | -------------------------------------------------------------------------------- /web/src/app/shared/models/index.model.ts: -------------------------------------------------------------------------------- 1 | export * from './factories/model.factory'; 2 | export * from './model.model'; 3 | export * from './activity.model'; 4 | export * from './breadcrumb.model'; 5 | export * from './contributor.model'; 6 | export * from './event.model'; 7 | export * from './help.model'; 8 | export * from './help-topic.model'; 9 | export * from './issue.model'; 10 | export * from './profile.model'; 11 | export * from './account.model'; 12 | export * from './loginAudit.model'; 13 | export * from './access.model'; 14 | export * from './build-times.model'; 15 | export * from './rating.model'; 16 | export * from './repository.model'; 17 | export * from './repositories.model'; 18 | export * from './monitor.model'; 19 | export * from './milestone.model'; 20 | export * from './ping.model'; 21 | export * from './project.model'; 22 | export * from './token.model'; 23 | export * from './pullRequest.model'; 24 | export * from './release.model'; 25 | export * from './stats.model'; 26 | export * from './status.model'; 27 | export * from './user.model'; 28 | export * from './userStats.model'; 29 | export * from './navigation.model'; 30 | -------------------------------------------------------------------------------- /web/src/app/shared/models/profile.model.ts: -------------------------------------------------------------------------------- 1 | // Application model 2 | import { Account } from './account.model'; 3 | import { ActivityModel } from './activity.model'; 4 | import { LoginAuditModel } from './loginAudit.model'; 5 | import { RepositoriesModel } from './repositories.model'; 6 | 7 | /** 8 | * Profile model 9 | */ 10 | export class ProfileModel { 11 | uid: string = ''; 12 | username: string; 13 | name: string = 'Guest'; 14 | email: string = ''; 15 | phone: string = ''; 16 | avatarUrl: string = ''; 17 | oauth?: { 18 | githubToken: string; 19 | token: string; 20 | expirationTime: string; 21 | authTime: string; 22 | issuedAtTime: string; 23 | signInProvider: string | null; 24 | claims: { 25 | [key: string]: any; 26 | } 27 | }; 28 | emailVerified: boolean = false; 29 | creationTime?: string; 30 | lastSignInTime?: string; 31 | account?: Account = new Account(); 32 | logins?: LoginAuditModel[] = []; 33 | repositories?: RepositoriesModel = new RepositoriesModel(); 34 | activity?: ActivityModel[] = []; 35 | following?: string[] = []; 36 | isAdmin?: boolean = false; 37 | } 38 | -------------------------------------------------------------------------------- /web/src/app/core/resolvers/tokens.resolver.ts: -------------------------------------------------------------------------------- 1 | // Angular modules 2 | import { Injectable } from '@angular/core'; 3 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; 4 | 5 | // Third party modules 6 | import { of, Observable } from 'rxjs'; 7 | import { catchError, take } from 'rxjs/operators'; 8 | 9 | // Dashboard hub model and services 10 | import { TokenService } from '@core/services/index.service'; 11 | import { TokenModel } from '@shared/models/index.model'; 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class TokensResolver implements Resolve { 17 | 18 | /** 19 | * Life cycle method 20 | * @param tokenService TokenService 21 | */ 22 | constructor( 23 | private tokenService: TokenService 24 | ) { } 25 | 26 | /** 27 | * Find all token data before displaying tokens page 28 | * @param route ActivatedRouteSnapshot 29 | */ 30 | resolve(route: ActivatedRouteSnapshot): Observable { 31 | return this.tokenService.findAll(route.params.projectUid) 32 | .pipe( 33 | take(1), 34 | catchError(() => of([])) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /functions/src/repository/update-repository.ts: -------------------------------------------------------------------------------- 1 | // Third party modules 2 | import { firestore, Change, CloudFunction, EventContext } from 'firebase-functions'; 3 | 4 | // Dashboard hub firebase functions models/mappers 5 | import { Logger } from '../client/logger'; 6 | import { RepositoryModel } from '../models/index.model'; 7 | import { DocumentData, DocumentSnapshot } from './../client/firebase-admin'; 8 | 9 | export const onUpdateRepository: CloudFunction> = firestore 10 | .document('repositories/{repositoryUid}') 11 | .onUpdate( async (change: Change, context: EventContext) => { 12 | 13 | try { 14 | const newData: DocumentData = change.after.data(); 15 | 16 | if (!newData.projects || Array.isArray(newData.projects) && newData.projects.length === 0) { 17 | Logger.info(`Delete repository ${context.params.repositoryUid}`); 18 | return RepositoryModel.getRepositoryReference(context.params.repositoryUid).delete(); 19 | } 20 | 21 | return newData; 22 | 23 | } catch (err) { 24 | Logger.error(err); 25 | throw new Error(err); 26 | } 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /web/src/app/core/resolvers/repository.resolver.ts: -------------------------------------------------------------------------------- 1 | // Angular modules 2 | import { Injectable } from '@angular/core'; 3 | import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; 4 | 5 | // 3rd party 6 | import { of, Observable } from 'rxjs'; 7 | import { catchError, take } from 'rxjs/operators'; 8 | 9 | // Dashboard hub model and services 10 | import { RepositoryService } from '@core/services/index.service'; 11 | import { RepositoryModel } from '@shared/models/index.model'; 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class RepositoryResolver implements Resolve { 17 | 18 | constructor( 19 | private respositoryService: RepositoryService, 20 | private router: Router 21 | ) { } 22 | 23 | resolve(route: ActivatedRouteSnapshot): Observable { 24 | return this.respositoryService.findOneById(route.params.repoUid) 25 | .pipe( 26 | take(1), 27 | catchError(() => { 28 | this.router.navigate(['/projects', route.params.projectUid]); 29 | 30 | return of(new RepositoryModel({ uid: '-' })); 31 | }) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /web/src/app/main/components/legal/privacy/privacy.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../../styles.scss"; 2 | @import "../../../../../assets/scss/variable"; 3 | 4 | 5 | .privacy { 6 | @include pageContainer; 7 | 8 | &__title { 9 | @include pageHeader; 10 | 11 | } 12 | 13 | &__card { 14 | max-width: 915px; 15 | @include cardContainer; 16 | margin-left: auto; 17 | margin-right: auto; 18 | 19 | &__header { 20 | @include header-icon; 21 | background-color: $red-color; 22 | box-shadow: 0px 3px 6px $red-shadow; 23 | } 24 | 25 | &__title { 26 | @include title; 27 | 28 | &__subtitle { 29 | @include subtitle; 30 | } 31 | } 32 | 33 | &__content { 34 | 35 | &__title { 36 | font-size: 22pt; 37 | padding: 15px 0; 38 | } 39 | 40 | &__text { 41 | color: $dark-grey-color; 42 | font-weight: lighter; 43 | font-size: 18px; 44 | } 45 | } 46 | 47 | &__actions { 48 | padding: 0 10px; 49 | 50 | &__disagree { 51 | background-color: #AAAAAA; 52 | color: #fff; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /web/src/app/shared/models/stats.model.ts: -------------------------------------------------------------------------------- 1 | // Application models 2 | import { IModel, Model } from './model.model'; 3 | 4 | /** 5 | * Stats interface 6 | */ 7 | export interface IStats extends IModel { 8 | projects?: number; 9 | users?: number; 10 | pings?: number; 11 | events?: number; 12 | } 13 | 14 | /** 15 | * Stats model 16 | */ 17 | export class StatsModel extends Model implements IStats { 18 | projects?: number; 19 | users?: number; 20 | pings?: number; 21 | events?: number; 22 | 23 | /** 24 | * Life cycle method 25 | * @param data data to be initialized 26 | */ 27 | constructor(data?: IStats) { 28 | super(); 29 | this.projects = data && data.projects ? data.projects : 0; 30 | this.users = data && data.users ? data.users : 0; 31 | this.pings = data && data.pings ? data.pings : 0; 32 | this.events = data && data.events ? data.events : 0; 33 | } 34 | 35 | /** 36 | * Method to convert model to data 37 | */ 38 | toData(): IStats { 39 | return { 40 | projects: this.projects, 41 | users: this.users, 42 | pings: this.pings, 43 | events: this.events, 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /web/test/integration/help/quickstart.feature: -------------------------------------------------------------------------------- 1 | Feature: Help section 2 | 3 | Scenario: Quickstart is listed on the help page 4 | Given the "/help" page is open 5 | Then the text "Quickstart" is in the element ".help__card__content__title" 6 | 7 | Scenario: Search for quickstart on help page 8 | Given the "/help" page is open 9 | When enter text "Quick" in the element ".help__card__header__search.mat-input-element" 10 | Then the text "Quickstart" is in the element ".help__card__content__title" 11 | And the ".help__card__content__title" link at position 0 is clicked 12 | Then the text "Get up and running in minutes" is in the element "#get-up-and-running-in-minutes-in-3-simple-steps-and-without-installing-anything" 13 | 14 | Scenario: Reset search on help page 15 | Given the "/help" page is open 16 | When enter text "Quickstart" in the element ".help__card__header__search.mat-input-element" 17 | Then the text "Glossary" is not in the element ".help__card__content__title" 18 | When clear text in the element ".help__card__header__search.mat-input-element" 19 | Then the text "Glossary" is in the element ".help__card__content__title" 20 | -------------------------------------------------------------------------------- /web/src/assets/scss/_variable.scss: -------------------------------------------------------------------------------- 1 | $grey-color: #eeeeee; 2 | $disable_color: #F0F0F0; 3 | $red-color: #DB4453; 4 | $orange-color: #F3A449; 5 | $orange-2-color: #FF5A00; 6 | $orange-3-color: #E9573E; 7 | $green-color: #8DC153; 8 | $green-2-color: #37BD9C; 9 | $blue-color: #4B89DC; 10 | $blue-2-color: #3BAEDA; 11 | $yellow-color: #f6bb43; 12 | $dark-grey-color: #979797; 13 | $dark-grey-2-color: #5E5E5E; 14 | $dark-grey-3-color: #4D4D4D; 15 | $purple-color: #967BDC; 16 | $border-color: rgba(0, 0, 0, 0.12); 17 | $backround-grey-light-color: #F7F7F7; 18 | $background-black-color: #4D4D4D; 19 | 20 | $red-shadow: rgba(219, 68, 83, 0.34); 21 | $orange-shadow: rgba(243, 164, 73, 0.5); 22 | $orange-2-shadow: rgba(255, 90, 0, 0.35); 23 | $orange-3-shadow: rgba(255, 90, 0, 0.35); 24 | $green-shadow: rgba(141, 193, 83, 0.45); 25 | $green-2-shadow: rgba(55, 189, 156, 0.5); 26 | $blue-shadow: rgba(59, 174, 218, 0.32); 27 | $blue-2-shadow: rgba(59, 174, 218, 0.32); 28 | $purple-shadow: rgba(151, 151, 151, 1); 29 | $dark-grey-shadow: rgba(151, 151, 151, 0.51); 30 | 31 | $breakpoint-feature-lg: 1410px; 32 | $breakpoint-lg: 1199px; 33 | $breakpoint-md: 960px; 34 | $breakpoint-sm: 768px; 35 | $breakpoint-xs: 576px; 36 | -------------------------------------------------------------------------------- /web/src/app/projects/projects.module.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | // Dashboard hub routing modules 6 | import { SharedModule } from '@shared/shared.module'; 7 | import { ProjectsRoutingModule } from './projects-routing.module'; 8 | 9 | // Dashboard hub components 10 | import { CreateEditProjectComponent } from './create-edit/create-edit.component'; 11 | import { ListMyProjectsComponent } from './list/list-my-projects.component'; 12 | import { MonitorSummaryComponent } from './monitor-summary/monitor-summary.component'; 13 | import { RatingComponent } from './rating/rating.component'; 14 | import { RepositoryComponent } from './repository/repository.component'; 15 | import { ViewProjectComponent } from './view/view.component'; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | ProjectsRoutingModule, 21 | SharedModule, 22 | ], 23 | declarations: [ 24 | CreateEditProjectComponent, 25 | MonitorSummaryComponent, 26 | ListMyProjectsComponent, 27 | RepositoryComponent, 28 | ViewProjectComponent, 29 | RatingComponent, 30 | ], 31 | }) 32 | export class ProjectsModule { } 33 | -------------------------------------------------------------------------------- /functions/src/user/events.ts: -------------------------------------------------------------------------------- 1 | import { DocumentReference, FirebaseAdmin, WriteBatch } from './../client/firebase-admin'; 2 | 3 | import { GitHubEventInput, GitHubEventMapper, GitHubEventModel } from '../mappers/github/index.mapper'; 4 | import { GitHubClient } from './../client/github'; 5 | import { Logger } from './../client/logger'; 6 | 7 | export interface EventsInput { 8 | token: string; 9 | username: string; 10 | } 11 | 12 | export const getUserEvents: any = async (token: string, uid: string, username: string) => { 13 | const batch: WriteBatch = FirebaseAdmin.firestore().batch(); 14 | const userRef: DocumentReference = FirebaseAdmin.firestore().collection('users').doc(uid); 15 | let events: GitHubEventInput[] = []; 16 | try { 17 | events = await GitHubClient(`/users/${username}/events`, token); 18 | } catch (error) { 19 | Logger.error(error); 20 | throw new Error(error); 21 | } 22 | const mappedEvents: GitHubEventModel[] = events.map((event: GitHubEventInput) => GitHubEventMapper.import(event)); 23 | 24 | await batch 25 | .update(userRef, { 26 | activity: mappedEvents, 27 | }); 28 | 29 | await batch.commit(); 30 | 31 | return mappedEvents; 32 | }; 33 | -------------------------------------------------------------------------------- /functions/src/client/firebase-admin.ts: -------------------------------------------------------------------------------- 1 | // Third party modules 2 | import * as admin from 'firebase-admin'; 3 | 4 | export const FirebaseAdmin: admin.app.App = admin.initializeApp(); 5 | 6 | export declare type DocumentSnapshot = admin.firestore.DocumentSnapshot; 7 | export declare type DocumentData = admin.firestore.DocumentData; 8 | export declare type WriteResult = admin.firestore.WriteResult; 9 | export declare type QuerySnapshot = admin.firestore.QuerySnapshot; 10 | export declare type QueryDocumentSnapshot = admin.firestore.QueryDocumentSnapshot; 11 | export declare type DocumentReference = admin.firestore.DocumentReference; 12 | export declare type Transaction = admin.firestore.Transaction; 13 | export declare type WriteBatch = admin.firestore.WriteBatch; 14 | export declare type FieldValue = admin.firestore.FieldValue; 15 | export declare type CollectionReference = admin.firestore.CollectionReference; 16 | export declare type Query = admin.firestore.Query; 17 | export declare type Firestore = admin.firestore.Firestore; 18 | 19 | // tslint:disable-next-line: typedef 20 | export const FieldPath = admin.firestore.FieldPath; 21 | 22 | export const IncrementFieldValue: FieldValue = admin.firestore.FieldValue.increment(1); 23 | 24 | -------------------------------------------------------------------------------- /web/src/app/shared/components/breadcrumb/breadcrumb.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | dashboard 6 | 7 | 8 | 9 | 10 | 13 | 16 | 17 | 18 | {{ typeIcon }} 19 | {{ item.title }} > 20 |
{{ subTitle }}
21 |
22 | -------------------------------------------------------------------------------- /web/src/app/core/resolvers/help.resolver.ts: -------------------------------------------------------------------------------- 1 | // Angular modules 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Injectable } from '@angular/core'; 4 | import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; 5 | 6 | // 3rd party 7 | import { of, Observable } from 'rxjs'; 8 | import { catchError, take } from 'rxjs/operators'; 9 | 10 | /** 11 | * Help resolver 12 | */ 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class HelpResolver implements Resolve { 17 | 18 | /** 19 | * Life cycle method 20 | * @param router Router 21 | * @param http HttpClient 22 | */ 23 | constructor( 24 | private router: Router, 25 | private http: HttpClient 26 | ) { } 27 | 28 | /** 29 | * Finds the help content in resolver 30 | * @param route ActivatedRouteSnapshot 31 | */ 32 | resolve(route: ActivatedRouteSnapshot): Observable { 33 | const path: string = route.params.path; 34 | 35 | return this.http.get(`/assets/help/${path}.md`, { responseType: 'text' }) 36 | .pipe( 37 | take(1), 38 | catchError(() => { 39 | this.router.navigate(['/help']); 40 | 41 | return of(''); 42 | }) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /web/src/app/shared/dialog/list/dialog-list.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component, Inject, OnDestroy } from '@angular/core'; 3 | import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; 4 | import { Subscription } from 'rxjs'; 5 | 6 | // Dashboard hub services 7 | import { ProjectModel, RepositoriesModel } from '../../models/index.model'; 8 | 9 | /** 10 | * Dialog list component 11 | */ 12 | @Component({ 13 | selector: 'dashboard-dialog-list', 14 | templateUrl: './dialog-list.component.html', 15 | styleUrls: ['./dialog-list.component.scss'], 16 | }) 17 | export class DialogListComponent implements OnDestroy { 18 | 19 | private repositorySubscription: Subscription; 20 | 21 | /** 22 | * Life cycle method 23 | * @param dialogRef MatDialogRef 24 | * @param data data to be send in dialog 25 | */ 26 | constructor( 27 | public dialogRef: MatDialogRef, 28 | @Inject(MAT_DIALOG_DATA) public data: { project: ProjectModel, repositories: RepositoriesModel } 29 | ) { } 30 | 31 | /** 32 | * Life cycle destroy method 33 | */ 34 | ngOnDestroy(): void { 35 | if (this.repositorySubscription) { 36 | this.repositorySubscription.unsubscribe(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /web/src/app/projects/monitor-summary/monitor-summary.component.ts: -------------------------------------------------------------------------------- 1 | // Core components 2 | import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; 3 | 4 | // Dashboard hub model 5 | import { MonitorModel } from '@shared/models/index.model'; 6 | 7 | /** 8 | * Monitor summary component 9 | */ 10 | @Component({ 11 | selector: 'dashboard-monitor-summary', 12 | templateUrl: './monitor-summary.component.html', 13 | styleUrls: ['./monitor-summary.component.scss'], 14 | }) 15 | export class MonitorSummaryComponent implements OnChanges { 16 | 17 | @Input('monitors') 18 | public monitors: MonitorModel[] = []; 19 | 20 | /** 21 | * Filter the monitors based upon the valid and invalid status 22 | * @param isValid isValid ping or not 23 | */ 24 | public filterBy(isValid: boolean): MonitorModel[] { 25 | return this.monitors.filter((monitor: MonitorModel) => { 26 | if (monitor && monitor.latestPing) { 27 | return monitor.latestPing.isValid === isValid; 28 | } 29 | }); 30 | } 31 | 32 | /** 33 | * Life cycle changes method 34 | * @param changes SimpleChanges 35 | */ 36 | ngOnChanges(changes: SimpleChanges): void { 37 | this.monitors = changes.monitors.currentValue; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /web/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | /*tslint:disable*/ 3 | import 'rxjs/Rx'; 4 | import 'zone.js/dist/long-stack-trace-zone'; 5 | import 'zone.js/dist/proxy.js'; 6 | import 'zone.js/dist/sync-test'; 7 | import 'zone.js/dist/jasmine-patch'; 8 | import 'zone.js/dist/async-test'; 9 | import 'zone.js/dist/fake-async-test'; 10 | import { getTestBed } from '@angular/core/testing'; 11 | import { 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting 14 | } from '@angular/platform-browser-dynamic/testing'; 15 | 16 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 17 | declare var __karma__: any; 18 | declare var require: any; 19 | 20 | // Prevent Karma from running prematurely. 21 | __karma__.loaded = function () {}; 22 | 23 | // First, initialize the Angular testing environment. 24 | getTestBed().initTestEnvironment( 25 | BrowserDynamicTestingModule, 26 | platformBrowserDynamicTesting() 27 | ); 28 | // Then we find all the tests. 29 | const context = require.context('./', true, /\.spec\.ts$/); 30 | // And load the modules. 31 | context.keys().map(context); 32 | // Finally, start Karma to run the tests. 33 | __karma__.start(); 34 | -------------------------------------------------------------------------------- /web/src/app/projects/rating/rating.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |

{{ repository.fullName }}

8 |

Rating Statistics

9 |
10 | 11 | 12 |
13 | 14 |
15 | {{ rating.name }} ({{ rating.value | number:'1.0-2' }}%) 16 | {{ rating.description }} 17 |
18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /web/src/app/core/services/application.service.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Injectable } from '@angular/core'; 3 | import { AngularFirestore } from '@angular/fire/firestore'; 4 | 5 | // Rxjs operators 6 | import { Observable } from 'rxjs'; 7 | import { map, switchMap } from 'rxjs/operators'; 8 | 9 | // Application model and services 10 | import { IStats, StatsModel } from '@shared/models/index.model'; 11 | import { ActivityService } from './activity.service'; 12 | 13 | /** 14 | * Application service 15 | */ 16 | @Injectable({ 17 | providedIn: 'root', 18 | }) 19 | export class ApplicationService { 20 | 21 | /** 22 | * Life cycle method 23 | * @param afs AngularFireStore 24 | * @param activityService ActivityService 25 | */ 26 | constructor( 27 | private afs: AngularFirestore, 28 | private activityService: ActivityService 29 | ) { } 30 | 31 | /** 32 | * Find the application stats 33 | */ 34 | public getApplicationStats(): Observable { 35 | return this.activityService 36 | .start() 37 | .pipe( 38 | switchMap(() => this.afs.collection('platform') 39 | .doc('stats') 40 | .valueChanges()), 41 | map((stats: IStats) => new StatsModel(stats)) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /web/src/app/main/components/legal/terms-conditions/terms-conditions.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../../assets/scss/variable"; 2 | @import "../../../../../assets/scss/mixin.scss"; 3 | 4 | .terms { 5 | @include pageContainer; 6 | 7 | &__title { 8 | @include pageHeader; 9 | } 10 | 11 | &__container { 12 | @include cardContainer; 13 | 14 | &__card { 15 | max-width: 915px; 16 | margin-left: auto; 17 | margin-right: auto; 18 | 19 | &__header { 20 | @include header-icon; 21 | background-color: $orange-2-color; 22 | box-shadow: 0px 3px 6px $orange-2-shadow; 23 | } 24 | 25 | &__title { 26 | @include title; 27 | 28 | &__subtitle { 29 | @include subtitle; 30 | } 31 | } 32 | 33 | &__content { 34 | 35 | &__title { 36 | font-size: 22pt; 37 | padding: 15px 0; 38 | } 39 | 40 | &__text { 41 | color: $dark-grey-color; 42 | font-weight: lighter; 43 | font-size: 18px; 44 | } 45 | } 46 | 47 | &__actions { 48 | padding: 0 10px; 49 | 50 | &__disagree { 51 | background-color: #AAAAAA; 52 | color: #fff; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /web/src/assets/help/create-project.md: -------------------------------------------------------------------------------- 1 | # What is an environment? 2 | An environment is a self-contained system where an entire programme or modules of an entire programme may be deployed and executed. It is common to have different environments within a phased development project, such as the Development Environment, the Staging Environment and the Production Environment. 3 | 4 | # How do I introduce an environment into **DashboardHub** ("create" an environment)? 5 | 1) Log in using your GitHub account by clicking the key icon on the top right of this page 6 | 7 | ![log in](https://user-images.githubusercontent.com/21239137/40269782-a1bad2b6-5b7a-11e8-8221-51298394317d.png) 8 | 9 | 2) Click plus icon on the top left 10 | 11 | ![Add Environment](https://user-images.githubusercontent.com/21239137/40558822-7be7d530-604c-11e8-9b39-4af1f9a06629.png) 12 | 13 | *Note: By clicking "Add Environment", you are adding a private, not public, environment.* 14 | 15 | # What is the difference between private and public environments? 16 | 17 | Public environments are environments that you can access by clicking "Public Environments" in the menu. They are environments you follow, but cannot make changes to or apply monitors or send pings on. 18 | 19 | Private environments are those that you can edit, applying monitors and send pings on. 20 | -------------------------------------------------------------------------------- /web/src/app/tokens/tokens-list/tokens-list.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../styles.scss"; 2 | @import "../../../assets/scss/variable"; 3 | 4 | .monitor-tokens { 5 | @include pageContainer; 6 | 7 | &__header { 8 | @include pageHeader; 9 | 10 | span { 11 | img { 12 | width: 32px; 13 | } 14 | 15 | mat-icon { 16 | font-size: 35px; 17 | } 18 | } 19 | 20 | &__add-button { 21 | color: #fff; 22 | background-color: $yellow-color; 23 | } 24 | } 25 | 26 | &__card { 27 | @include cardContainer; 28 | 29 | &__header { 30 | @include header-icon; 31 | background-color: $yellow-color; 32 | box-shadow: 0px 3px 6px $orange-3-shadow; 33 | } 34 | 35 | &__title { 36 | @include title; 37 | width: 30vw; 38 | 39 | &__subtitle { 40 | @include subtitle; 41 | width: 35vw; 42 | } 43 | } 44 | 45 | &__content { 46 | padding: 0; 47 | display: block; 48 | overflow: hidden; 49 | @include tableContainer; 50 | 51 | &__table { 52 | width: 100%; 53 | 54 | &__name { 55 | word-break: break-all; 56 | max-width: 60vw; 57 | } 58 | 59 | &__action { 60 | cursor: pointer; 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /functions/src/mappers/github/milestone.mapper.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | 3 | // Dashboard mappers/models 4 | import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './index.mapper'; 5 | 6 | export interface GitHubMilestoneInput { 7 | id: number; 8 | title: string; 9 | creator: GitHubUserInput; 10 | state: string; 11 | open_issues: number; 12 | closed_issues: number; 13 | html_url: string; 14 | description: string; 15 | updated_at: string; 16 | } 17 | 18 | export interface GitHubMilestoneModel { 19 | uid: number; 20 | title: string; 21 | creator: GitHubUserModel; 22 | state: string; 23 | openIssues: number; 24 | closeIssues: number; 25 | htmlUrl: string; 26 | description: string; 27 | updatedAt: firestore.Timestamp; 28 | } 29 | 30 | export class GitHubMilestoneMapper { 31 | static import(input: GitHubMilestoneInput): GitHubMilestoneModel { 32 | return { 33 | uid: input.id, 34 | title: input.title, 35 | creator: GitHubUserMapper.import(input.creator), 36 | state: input.state, 37 | openIssues: input.open_issues, 38 | closeIssues: input.closed_issues, 39 | htmlUrl: input.html_url, 40 | description: input.description, 41 | updatedAt: firestore.Timestamp.fromDate(new Date(input.updated_at)), 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /web/src/app/core/resolvers/edit-token.resolver.ts: -------------------------------------------------------------------------------- 1 | // Angular modules 2 | import { Injectable } from '@angular/core'; 3 | import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; 4 | 5 | // Third party modules 6 | import { of, Observable } from 'rxjs'; 7 | import { catchError, take } from 'rxjs/operators'; 8 | 9 | // Dashboard hub model and services 10 | import { TokenService } from '@core/services/index.service'; 11 | import { TokenModel } from '@shared/models/index.model'; 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class EditTokenResolver implements Resolve { 17 | 18 | /** 19 | * Life cycle method 20 | * @param tokenService TokenService 21 | * @param router Router 22 | */ 23 | constructor( 24 | private tokenService: TokenService, 25 | private router: Router 26 | ) { } 27 | 28 | /** 29 | * Find the token data before displaying on the page 30 | * @param route ActivatedRouteSnapshot 31 | */ 32 | resolve(route: ActivatedRouteSnapshot): Observable { 33 | return this.tokenService.findOneById(route.params.projectUid, route.params.uid) 34 | .pipe( 35 | take(1), 36 | catchError(() => { 37 | this.router.navigate(['/']); 38 | 39 | return of(new TokenModel()); 40 | }) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web/src/app/main/components/stats/stats.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../assets/scss/variable"; 2 | @import "../../../../assets/scss/mixin.scss"; 3 | .stats { 4 | 5 | &__title { 6 | @include pageHeader; 7 | 8 | } 9 | 10 | &__card { 11 | @include cardContainer; 12 | 13 | &__header { 14 | @include header-icon; 15 | background-color: $green-2-color; 16 | box-shadow: 0px 3px 6px $green-2-shadow; 17 | } 18 | 19 | &__divider{ 20 | margin-top: 20px; 21 | } 22 | 23 | &__title { 24 | @include title; 25 | 26 | &__subtitle { 27 | @include subtitle; 28 | } 29 | } 30 | 31 | &__content { 32 | padding: 45px 0px; 33 | display: block; 34 | overflow: hidden; 35 | 36 | &__item { 37 | font-size: 18px; 38 | 39 | &__blue { 40 | border-bottom: 2px solid $blue-2-color; 41 | } 42 | 43 | &__red { 44 | border-bottom: 2px solid $red-color; 45 | } 46 | 47 | &__purple { 48 | border-bottom: 2px solid $purple-color; 49 | } 50 | 51 | &__dark-blue { 52 | border-bottom: 2px solid $blue-color; 53 | } 54 | 55 | &__number { 56 | padding-right: 10px; 57 | font-weight: 500; 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /web/src/app/admin/users-list/users-list.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component, OnDestroy, OnInit } from '@angular/core'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | // DashboardHub Model and services 6 | import { UserService } from '@core/services/user.service'; 7 | import { UserModel } from '@shared/models/user.model'; 8 | 9 | /** 10 | * Users list component 11 | */ 12 | @Component({ 13 | selector: 'dashboard-users-list', 14 | templateUrl: './users-list.component.html', 15 | styleUrls: ['./users-list.component.scss'], 16 | }) 17 | export class UsersListComponent implements OnInit, OnDestroy { 18 | 19 | public activeuserTable: string[] = ['avatar', 'name', 'creationTime', 'lastSignInTime']; 20 | public userSubscription: Subscription; 21 | public users: UserModel[]; 22 | 23 | /** 24 | * Life cycle method 25 | * @param userService UserService 26 | */ 27 | constructor( 28 | private userService: UserService 29 | ) { } 30 | 31 | /** 32 | * Life cycle init method 33 | */ 34 | ngOnInit(): void { 35 | this.userSubscription = this.userService 36 | .findAllUserList() 37 | .subscribe((users: UserModel[]) => this.users = users); 38 | } 39 | 40 | /** 41 | * Lifecycle destroy method 42 | */ 43 | ngOnDestroy(): void { 44 | this.userSubscription.unsubscribe(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /web/src/app/main/components/help-detail/help-detail.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | 5 | // Application models 6 | import { HelpModel, HelpTopic } from '@shared/models/index.model'; 7 | 8 | @Component({ 9 | selector: 'dashboard-help-detail', 10 | templateUrl: './help-detail.component.html', 11 | styleUrls: ['./help-detail.component.scss'], 12 | encapsulation: ViewEncapsulation.None, 13 | }) 14 | export class HelpDetailComponent implements OnInit { 15 | 16 | public data: HelpModel = new HelpModel(); 17 | 18 | /** 19 | * Life cycle method 20 | * @param route ActivatedRoute instance 21 | */ 22 | constructor( 23 | private route: ActivatedRoute 24 | ) { } 25 | 26 | /** 27 | * Life cycle init method 28 | * Find the topics based upon the help path and display it in help detail component 29 | */ 30 | ngOnInit(): void { 31 | this.route.data.subscribe((data: string[]) => this.data.content = data[0]); 32 | const path: string = this.route.snapshot.paramMap.get('path'); 33 | const topics: HelpTopic[] = this.data.topics; 34 | const filteredTopic: HelpTopic = topics.find((topic: HelpTopic) => topic.path === path); 35 | this.data.icon = filteredTopic.icon; 36 | this.data.title = filteredTopic.title; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /web/src/app/core/resolvers/pings.resolver.ts: -------------------------------------------------------------------------------- 1 | // Angular modules 2 | import { Injectable } from '@angular/core'; 3 | import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; 4 | 5 | // 3rd party 6 | import { of, Observable } from 'rxjs'; 7 | import { catchError, take } from 'rxjs/operators'; 8 | 9 | // DashboardHub model and services 10 | import { PingService } from '@core/services/index.service'; 11 | import { PingModel } from '@shared/models/index.model'; 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class PingsResolver implements Resolve { 17 | 18 | /** 19 | * Life cycle method 20 | * @param pingService PingService 21 | * @param router Router 22 | */ 23 | constructor( 24 | private pingService: PingService, 25 | private router: Router 26 | ) { } 27 | 28 | /** 29 | * Find all monitors before displaying data on pings page and navigate to monitors page in case of error 30 | * @param route ActivatedRouteSnapshot 31 | */ 32 | resolve(route: ActivatedRouteSnapshot): Observable { 33 | return this.pingService.findAllByMonitor(route.params.projectUid, route.params.monitorUid) 34 | .pipe( 35 | take(1), 36 | catchError(() => { 37 | this.router.navigate(['/projects', route.params.projectUid, 'monitors']); 38 | 39 | return of([]); 40 | }) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web/src/app/core/interceptors/github.http.interceptor.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HTTP_INTERCEPTORS } from '@angular/common/http'; 3 | import { Injectable, NgModule } from '@angular/core'; 4 | import { Observable } from 'rxjs'; 5 | 6 | // Dashboard hub services 7 | import { AuthenticationService } from '@core/services/index.service'; 8 | 9 | @Injectable() 10 | export class GitHubHttpInterceptor implements HttpInterceptor { 11 | 12 | /** 13 | * Life cycle method 14 | * @param authService AuthenticationService 15 | */ 16 | constructor(private authService: AuthenticationService) { 17 | } 18 | 19 | /** 20 | * Send github token in all github requests 21 | * @param req HttpRequest 22 | * @param next HttpHandler 23 | */ 24 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 25 | if (req.url.includes('github.com')) { 26 | req = req.clone({ 27 | setHeaders: { 28 | Authorization: `token ${this.authService.profile.oauth.githubToken}`, 29 | Accept: 'application/vnd.github.v3+json', 30 | }, 31 | }); 32 | } 33 | 34 | return next.handle(req); 35 | } 36 | 37 | } 38 | 39 | @NgModule({ 40 | providers: [ 41 | { 42 | provide: HTTP_INTERCEPTORS, 43 | useClass: GitHubHttpInterceptor, 44 | multi: true, 45 | }, 46 | ], 47 | }) 48 | export class GitHubHttpInterceptorModule { 49 | } 50 | -------------------------------------------------------------------------------- /web/src/app/monitors/monitors-routing.module.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | 5 | // Dashboard hub components 6 | import { EditProjectResolver } from '@core/resolvers/edit-project.resolver'; 7 | import { PingsResolver } from '@core/resolvers/pings.resolver'; 8 | import { ViewProjectResolver } from '@core/resolvers/view-project.resolver'; 9 | import { MonitorCreateEditComponent } from './monitor-create-edit/monitor-create-edit.component'; 10 | import { MonitorsListComponent } from './monitors-list/monitors-list.component'; 11 | import { PingsListComponent } from './pings-list/pings-list.component'; 12 | 13 | const routes: Routes = [ 14 | { 15 | path: '', 16 | component: MonitorsListComponent, 17 | resolve: { project: ViewProjectResolver }, 18 | }, 19 | { 20 | path: 'create', 21 | component: MonitorCreateEditComponent, 22 | resolve: { project: EditProjectResolver }, 23 | }, 24 | { 25 | path: ':monitorUid', 26 | component: MonitorCreateEditComponent, 27 | resolve: { project: EditProjectResolver }, 28 | }, 29 | { 30 | path: ':monitorUid/pings', 31 | component: PingsListComponent, 32 | resolve: { pings: PingsResolver, project: ViewProjectResolver }, 33 | }, 34 | ]; 35 | 36 | @NgModule({ 37 | imports: [RouterModule.forChild(routes)], 38 | exports: [RouterModule], 39 | }) 40 | export class MonitorsRoutingModule { } 41 | -------------------------------------------------------------------------------- /web/src/app/tokens/tokens-routing.module.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | 5 | // Dashboard hub components 6 | import { EditTokenResolver } from '@core/resolvers/edit-token.resolver'; 7 | import { TokensResolver } from '@core/resolvers/tokens.resolver'; 8 | import { TokensListComponent } from './tokens-list/tokens-list.component'; 9 | 10 | // Dashboard hub authentication guards 11 | import { ViewProjectResolver } from '@app/core/resolvers/view-project.resolver'; 12 | import { AuthGuard } from '@core/guards/authentication.guard'; 13 | import { TokensCreateEditComponent } from './tokens-create-edit/tokens-create-edit.component'; 14 | 15 | const routes: Routes = [ 16 | { 17 | path: '', 18 | component: TokensListComponent, 19 | canActivate: [AuthGuard], 20 | resolve: { tokens: TokensResolver, project: ViewProjectResolver }, 21 | }, 22 | { 23 | path: 'create', 24 | component: TokensCreateEditComponent, 25 | canActivate: [AuthGuard], 26 | resolve: { project: ViewProjectResolver }, 27 | }, 28 | { 29 | path: ':uid/edit', 30 | component: TokensCreateEditComponent, 31 | canActivate: [AuthGuard], 32 | resolve: { token: EditTokenResolver, project: ViewProjectResolver }, 33 | }, 34 | ]; 35 | 36 | @NgModule({ 37 | imports: [RouterModule.forChild(routes)], 38 | exports: [RouterModule], 39 | }) 40 | export class TokensRoutingModule { } 41 | -------------------------------------------------------------------------------- /web/src/app/shared/models/token.model.ts: -------------------------------------------------------------------------------- 1 | // Third party modules 2 | import { firestore } from 'firebase'; 3 | 4 | // DashboardHub models 5 | import { IModel, Model } from './model.model'; 6 | 7 | /** 8 | * Token interface 9 | */ 10 | export interface IToken extends IModel { 11 | uid: string; 12 | name: string; 13 | createdOn?: firestore.Timestamp; 14 | updatedOn?: firestore.Timestamp; 15 | } 16 | 17 | /** 18 | * Token model 19 | */ 20 | export class TokenModel extends Model implements IToken { 21 | uid: string; 22 | name: string; 23 | createdOn: firestore.Timestamp; 24 | updatedOn: firestore.Timestamp; 25 | 26 | /** 27 | * Life cycle method 28 | * @param token token to initialize 29 | */ 30 | constructor(token?: IToken) { 31 | super(); 32 | this.uid = token ? token.uid : undefined; 33 | this.name = token ? token.name : undefined; 34 | this.createdOn = token ? token.createdOn : undefined; 35 | this.updatedOn = token ? token.updatedOn : undefined; 36 | } 37 | 38 | /** 39 | * Method to covert model to data 40 | * @param requiredOnly parameter to check if required token or not 41 | */ 42 | public toData(requiredOnly?: boolean): IToken { 43 | const data: IToken = { 44 | uid: this.uid, 45 | name: this.name, 46 | }; 47 | 48 | if (!requiredOnly) { 49 | data.createdOn = this.createdOn; 50 | data.updatedOn = this.updatedOn; 51 | } 52 | 53 | return data; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /web/src/assets/scss/mixin.scss: -------------------------------------------------------------------------------- 1 | @import "./variable"; 2 | 3 | @mixin header-icon { 4 | position: absolute; 5 | padding-top: 18px; 6 | padding-left: 13px; 7 | margin-top: -30px !important; 8 | margin-right: 15px; 9 | float: left; 10 | border-radius: 5px; 11 | opacity: 1; 12 | padding-bottom: 7px; 13 | padding-right: 13px; 14 | 15 | mat-icon { 16 | font-size: 39px; 17 | line-height: 24px; 18 | width: 39px; 19 | height: 39px; 20 | text-align: center; 21 | color: white; 22 | } 23 | } 24 | 25 | @mixin subtitle { 26 | @include title; 27 | font-size: 14px; 28 | } 29 | 30 | @mixin title { 31 | @include ellipsisText; 32 | padding-left: 60px; 33 | width: 55vw; 34 | font-weight: lighter; 35 | 36 | @media (max-width: $breakpoint-md) { 37 | width: 45vw; 38 | } 39 | 40 | @media (max-width: $breakpoint-xs) { 41 | width: 55vw; 42 | } 43 | } 44 | 45 | @mixin pageHeader { 46 | font-size: 20px; 47 | } 48 | 49 | @mixin pageContainer { 50 | padding: 20px 30px; 51 | 52 | @media (max-width: $breakpoint-xs) { 53 | padding: 15px 20px; 54 | } 55 | } 56 | 57 | @mixin cardContainer { 58 | margin-top: 55px; 59 | 60 | @media (max-width: $breakpoint-xs) { 61 | margin-top: 45px; 62 | } 63 | } 64 | 65 | @mixin ellipsisText { 66 | overflow: hidden; 67 | text-overflow: ellipsis; 68 | white-space: nowrap; 69 | } 70 | 71 | @mixin tableContainer { 72 | margin-left: -16px; 73 | margin-right: -16px; 74 | } 75 | -------------------------------------------------------------------------------- /functions/src/mappers/github/issue.mapper.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | 3 | // Dashboard mappers/models 4 | import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './user.mapper'; 5 | 6 | export interface GitHubIssueInput { 7 | id: number; 8 | html_url: string; 9 | state: string; 10 | title: string; 11 | number: number; 12 | body: string; 13 | user: GitHubUserInput; 14 | assignees: GitHubUserInput[]; 15 | created_at: string; 16 | updated_at: string; 17 | } 18 | 19 | export interface GitHubIssueModel { 20 | uid: number; 21 | url: string; 22 | state: string; 23 | title: string; 24 | number: number; 25 | description: string; 26 | owner: GitHubUserModel; 27 | assignees: GitHubUserModel[]; 28 | createdOn: firestore.Timestamp; 29 | updatedOn: firestore.Timestamp; 30 | } 31 | 32 | export class GitHubIssueMapper { 33 | static import(input: GitHubIssueInput): GitHubIssueModel { 34 | return { 35 | uid: input.id, 36 | url: input.html_url, 37 | state: input.state, 38 | title: input.title, 39 | number: input.number, 40 | description: input.body, 41 | owner: GitHubUserMapper.import(input.user), 42 | assignees: input.assignees.map((assignee: GitHubUserInput) => GitHubUserMapper.import(assignee)), 43 | createdOn: firestore.Timestamp.fromDate(new Date(input.created_at)), 44 | updatedOn: firestore.Timestamp.fromDate(new Date(input.updated_at)), 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to DashboardHub 2 | 3 | Thank you for taking the time to contribute. 4 | 5 | The DashboardHub community is a safe place for people to collaborate. 6 | 7 | Please read the [CODE of CONDUCT](/CODE_OF_CONDUCT.md). 8 | 9 | --- 10 | 11 | ## All contributions are welcome 12 | 13 | ...from typos in documentation to coding new features. If you require any help or have any questions please do not hesitate to ask, we are friendly and aim to reply within 48hours. 14 | 15 | --- 16 | 17 | ## Commit messages 18 | 19 | We are now using [Conventional Commits specification](https://conventionalcommits.org) 20 | 21 | ### Commit Message Convention, at a Glance 22 | 23 | Dont forget to include the **Issue Number**... 24 | 25 | _patches:_ 26 | 27 | ```sh 28 | git commit -a -m "fix(parsing): #123 fixed a bug in our parser" 29 | ``` 30 | 31 | _features:_ 32 | 33 | ```sh 34 | git commit -a -m "feat(parser): #123 we now have a parser \o/" 35 | ``` 36 | 37 | _breaking changes:_ 38 | 39 | ```sh 40 | git commit -a -m "feat(new-parser): #123 introduces a new parsing library 41 | BREAKING CHANGE: new library does not support foo-construct" 42 | ``` 43 | 44 | _other changes:_ 45 | 46 | You decide, e.g., docs, chore, etc. 47 | 48 | ```sh 49 | git commit -a -m "docs: #123 fixed up the docs a bit" 50 | ``` 51 | 52 | _but wait, there's more!_ 53 | 54 | Github usernames (`@bcoe`) and issue references (#133) will be swapped out for the 55 | appropriate URLs in your CHANGELOG. 56 | -------------------------------------------------------------------------------- /web/src/app/main/components/legal/privacy/privacy.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Privacy
4 |
5 |
6 | 7 | 8 |
9 | lock 10 |
11 | Privacy 12 | Legal 13 |
14 | 15 | 16 |
Introduction
17 |
This agreement is between you and DashboardHub. We are the owner and operator of DashboardHub (the Service).
18 |
19 | 20 |
21 | 22 | 25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /web/src/app/shared/components/private-public-project/private-public-project.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
My Projects
4 | 5 | 9 | 13 | 14 |
15 | 16 | 17 |
18 | dashboard 19 |
20 | 21 | {{ title }} 22 | 23 | You have {{ projects.length }} projects 24 |
25 | 26 | 27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /functions/src/user/stats.ts: -------------------------------------------------------------------------------- 1 | // Third party modules 2 | import * as admin from 'firebase-admin'; 3 | import { firestore, Change, EventContext } from 'firebase-functions'; 4 | 5 | // Dashboard hub firebase functions models/mappers 6 | import { DocumentData, DocumentSnapshot, FirebaseAdmin } from './../client/firebase-admin'; 7 | import { Logger } from './../client/logger'; 8 | import { GitHubUserStatsModel } from './../mappers/github/user.mapper'; 9 | 10 | export const onUpdateUserStats: any = firestore 11 | .document('users/{userId}') 12 | .onWrite((change: Change, context: EventContext) => { 13 | const user: DocumentData = change.after.data(); 14 | 15 | if (!user) { 16 | // TODO delete 17 | return null; 18 | } 19 | 20 | const data: GitHubUserStatsModel = { 21 | name: user.name, 22 | username: user.username, 23 | avatarUrl: user.avatarUrl, 24 | github: { 25 | repository: { 26 | total: user.repositories && user.repositories.data ? user.repositories.data.length : 0, 27 | }, 28 | activity: { 29 | latest: user.activity ? user.activity[0] : {}, 30 | }, 31 | }, 32 | lastUpdated: admin.firestore.Timestamp.fromDate(new Date()), 33 | }; 34 | 35 | Logger.info({ 36 | user: user.username, 37 | imported: {}, 38 | }); 39 | 40 | return FirebaseAdmin 41 | .firestore() 42 | .collection('userStats') 43 | .doc(user.uid) 44 | .set(data, { merge: true }); 45 | }); 46 | -------------------------------------------------------------------------------- /web/src/app/core/services/ping.service.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Injectable } from '@angular/core'; 3 | import { AngularFirestore } from '@angular/fire/firestore'; 4 | import { Observable } from 'rxjs'; 5 | import { switchMap } from 'rxjs/operators'; 6 | 7 | // Dashboard model and services 8 | import { IProject, PingModel } from '@shared/models/index.model'; 9 | import { ActivityService } from './activity.service'; 10 | 11 | /** 12 | * Ping service 13 | */ 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class PingService { 18 | 19 | /** 20 | * Lifecycle method 21 | * @param afs AngularFirestore 22 | * @param activityService ActivityService 23 | */ 24 | constructor( 25 | private afs: AngularFirestore, 26 | private activityService: ActivityService 27 | ) { 28 | } 29 | 30 | /** 31 | * Find all the pings by monitor using monitorUid 32 | * @param projectUid uid of project 33 | * @param monitorUid uid of monitor 34 | */ 35 | public findAllByMonitor(projectUid: string, monitorUid: string): Observable { 36 | return this.activityService 37 | .start() 38 | .pipe( 39 | switchMap(() => this.afs 40 | .collection('projects') 41 | .doc(projectUid) 42 | .collection('pings', 43 | (ref: firebase.firestore.Query) => ref.where('monitorUid', '==', monitorUid) 44 | .orderBy('createdOn', 'desc') 45 | ) 46 | .valueChanges()) 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /web/src/app/main/main.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | // Dashboard hub routing modules 5 | import { SharedModule } from '@shared/shared.module'; 6 | import { MainRoutingModule } from './main-routing.module'; 7 | 8 | // Dashboard hub components 9 | import { FeaturesComponent } from './components/features/features.component'; 10 | import { FollowingComponent } from './components/following/following.component'; 11 | import { HelpDetailComponent } from './components/help-detail/help-detail.component'; 12 | import { HelpComponent } from './components/help/help.component'; 13 | import { HomepageComponent } from './components/homepage/homepage.component'; 14 | import { PrivacyComponent } from './components/legal/privacy/privacy.component'; 15 | import { TermsConditionsComponent } from './components/legal/terms-conditions/terms-conditions.component'; 16 | import { ProfileComponent } from './components/profile/profile.component'; 17 | import { StatsComponent } from './components/stats/stats.component'; 18 | import { MainComponent } from './main.component'; 19 | 20 | @NgModule({ 21 | imports: [ 22 | CommonModule, 23 | MainRoutingModule, 24 | SharedModule, 25 | ], 26 | declarations: [MainComponent, 27 | FeaturesComponent, 28 | HelpComponent, 29 | HelpDetailComponent, 30 | HomepageComponent, 31 | PrivacyComponent, 32 | ProfileComponent, 33 | TermsConditionsComponent, 34 | StatsComponent, 35 | FollowingComponent, 36 | ], 37 | }) 38 | export class MainModule { } 39 | -------------------------------------------------------------------------------- /web/src/app/main/components/help/help.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../styles.scss"; 2 | @import "../../../../assets/scss/variable"; 3 | 4 | 5 | .help { 6 | @include pageContainer; 7 | 8 | &__title { 9 | @include pageHeader; 10 | } 11 | 12 | &__card { 13 | @include cardContainer; 14 | 15 | &__header { 16 | @include header-icon; 17 | left: 20px; 18 | right: 20px; 19 | padding: 5px; 20 | margin-right: 0px; 21 | background: $purple-color; 22 | box-shadow: none; 23 | 24 | &__search { 25 | color: #fff; 26 | padding: 0 0; 27 | } 28 | 29 | ; 30 | } 31 | 32 | &__title { 33 | @include title; 34 | 35 | &__subtitle { 36 | @include subtitle; 37 | } 38 | } 39 | 40 | &__content { 41 | padding-top: 60px; 42 | color: $dark-grey-color; 43 | 44 | &__icon { 45 | min-width: 24px; 46 | min-height: 24px; 47 | padding-right: 30px; 48 | } 49 | 50 | &__title { 51 | color: #000; 52 | font-size: 20px; 53 | } 54 | 55 | &__description { 56 | color: $dark-grey-color; 57 | font-weight: lighter; 58 | font-size: 16px; 59 | } 60 | 61 | &__time { 62 | color: $dark-grey-color; 63 | font-weight: lighter; 64 | font-size: 14px; 65 | } 66 | } 67 | 68 | &__actions { 69 | padding: 0 10px; 70 | 71 | &__disagree { 72 | background-color: #AAAAAA; 73 | color: #fff; 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /web/src/app/main/components/legal/terms-conditions/terms-conditions.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Terms & Conditions
4 |
5 |
6 |
7 | 8 | 9 |
10 | event_note 11 |
12 | Terms and Conditions 13 | Legal 14 |
15 | 16 | 17 |
TBC
18 |
Coming soon....
19 |
20 | 21 |
22 | 23 | 26 |
27 |
28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /web/src/app/core/services/activity.service.ts: -------------------------------------------------------------------------------- 1 | // angular core imports 2 | import { Injectable } from '@angular/core'; 3 | import { of, timer, Observable, Subject } from 'rxjs'; 4 | import { finalize, tap } from 'rxjs/operators'; 5 | 6 | /** 7 | * Activity service 8 | */ 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class ActivityService { 13 | 14 | private counter: number = 0; 15 | private subject: Subject = new Subject(); 16 | 17 | /** 18 | * Retrieve the status of progress bar 19 | */ 20 | public getProgressBar(): Observable { 21 | return this.subject.asObservable(); 22 | } 23 | 24 | /** 25 | * Change status of the progress bar 26 | * 27 | * @param {boolean} status enable/disable progress bar 28 | */ 29 | public setProgressBar(status: boolean): number { 30 | if (status) { 31 | this.counter++; 32 | } 33 | 34 | if (!status) { 35 | this.counter--; 36 | } 37 | 38 | // activity bar disappears too quickly, delay the final step 39 | if (this.counter === 0 && status === false) { 40 | timer(500).subscribe(() => this.subject.next(this.counter)); 41 | } else { 42 | this.subject.next(this.counter); 43 | } 44 | 45 | return this.counter; 46 | } 47 | 48 | /** 49 | * Starts the oberservable chain and finishes when complete 50 | */ 51 | public start(): Observable { 52 | return of(0) 53 | .pipe( 54 | tap(() => this.setProgressBar(true)), 55 | finalize(() => this.setProgressBar(false)) 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /web/src/app/main/components/following/following.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component, OnInit } from '@angular/core'; 3 | 4 | // Rxjs modules 5 | import { combineLatest } from 'rxjs'; 6 | import { filter, mergeMap } from 'rxjs/operators'; 7 | 8 | // DashboardHub Service and models 9 | import { AuthenticationService, ProjectService } from '@core/services/index.service'; 10 | import { ProfileModel, ProjectModel } from '@shared/models/index.model'; 11 | 12 | @Component({ 13 | selector: 'dashboard-following', 14 | templateUrl: './following.component.html', 15 | styleUrls: ['./following.component.scss'], 16 | }) 17 | export class FollowingComponent implements OnInit { 18 | 19 | public projects: ProjectModel[] = []; 20 | public title: string = 'Following projects list'; 21 | 22 | /** 23 | * Life cycle method 24 | * @param authenticationService AuthenticationService 25 | * @param projectService ProjectService 26 | */ 27 | constructor( 28 | private authenticationService: AuthenticationService, 29 | private projectService: ProjectService 30 | ) { } 31 | 32 | /** 33 | * Finds the all projects details followed by logged in user 34 | */ 35 | ngOnInit(): void { 36 | this.authenticationService.getCurrentUser() 37 | .pipe( 38 | filter((profile: ProfileModel) => !!(profile.following && profile.following.length)), 39 | mergeMap((profile: ProfileModel) => combineLatest(...profile.following.map((uid: string) => this.projectService.findOneById(uid)))) 40 | ) 41 | .subscribe((projects: ProjectModel[]) => this.projects = projects); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web/src/app/admin/users-list/users-list.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles.scss'; 2 | @import '../../../assets/scss/variable'; 3 | 4 | .users { 5 | padding: 20px 30px; 6 | @include pageContainer; 7 | 8 | &__title { 9 | @include pageHeader; 10 | } 11 | 12 | &__users-card { 13 | @include cardContainer; 14 | 15 | &__header { 16 | @include header-icon; 17 | background-color: $purple-color; 18 | box-shadow: 0px 3px 6px $purple-shadow; 19 | } 20 | 21 | &__title { 22 | @include title; 23 | 24 | &__subtitle { 25 | @include subtitle; 26 | } 27 | } 28 | 29 | &__content { 30 | padding: 0; 31 | display: block; 32 | overflow: hidden; 33 | @include tableContainer; 34 | 35 | &__table { 36 | width: 100%; 37 | padding: 0; 38 | 39 | &__avatar { 40 | height: 35px; 41 | border-radius: 50%; 42 | object-fit: cover; 43 | border: 1px solid $orange-color; 44 | margin-right: 20px; 45 | } 46 | 47 | @media (max-width: $breakpoint-xs) { 48 | &__avatar { 49 | margin-right: 10px; 50 | } 51 | } 52 | 53 | &__description { 54 | margin-right: 5px; 55 | } 56 | 57 | &__title { 58 | min-width: 5vw; 59 | padding-right: 10px; 60 | } 61 | 62 | &__time { 63 | color: $dark-grey-color; 64 | font-size: 14px; 65 | font-weight: lighter; 66 | @include ellipsisText; 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /functions/src/shared/delete-collection.ts: -------------------------------------------------------------------------------- 1 | // Dashboard hub firebase functions models/mappers 2 | import { Firestore, Query, QueryDocumentSnapshot, QuerySnapshot, WriteBatch } from '../client/firebase-admin'; 3 | 4 | /** 5 | * From https://firebase.google.com/docs/firestore/manage-data/delete-data 6 | */ 7 | export function deleteCollection(db: Firestore, collectionPath: string, batchSize: number): Promise { 8 | const query: Query = db.collection(collectionPath).limit(batchSize); 9 | 10 | return new Promise((resolve: (value?: any) => void, reject: (reason?: any) => void) => { 11 | deleteQueryBatch(db, query, batchSize, resolve, reject); 12 | }); 13 | } 14 | 15 | function deleteQueryBatch(db: Firestore, query: Query, batchSize: number, resolve: (value?: any) => void, reject: (reason?: any) => void) { 16 | query.get() 17 | .then((snapshot: QuerySnapshot) => { 18 | // When there are no documents left, we are done 19 | if (snapshot.size === 0) { 20 | return snapshot.size; 21 | } 22 | 23 | // Delete documents in a batch 24 | const batch: WriteBatch = db.batch(); 25 | snapshot.docs.forEach((doc: QueryDocumentSnapshot) => batch.delete(doc.ref)); 26 | 27 | return batch.commit().then(() => snapshot.size); 28 | }) 29 | .then((numDeleted: number) => { 30 | if (numDeleted === 0) { 31 | resolve(); 32 | return; 33 | } 34 | 35 | // Recurse on the next process tick, to avoid 36 | // exploding the stack. 37 | process.nextTick(() => deleteQueryBatch(db, query, batchSize, resolve, reject)); 38 | }) 39 | .catch(reject); 40 | } 41 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/member.ts: -------------------------------------------------------------------------------- 1 | import { isExistProperties, Repository, User } from './shared'; 2 | 3 | interface Member { 4 | login: string; 5 | id: number; 6 | node_id: string; 7 | avatar_url: string; 8 | gravatar_id: string; 9 | url: string; 10 | html_url: string; 11 | followers_url: string; 12 | following_url: string; 13 | gists_url: string; 14 | starred_url: string; 15 | subscriptions_url: string; 16 | organizations_url: string; 17 | repos_url: string; 18 | events_url: string; 19 | received_events_url: string; 20 | type: string; 21 | site_admin: boolean; 22 | } 23 | 24 | export interface MemberEventInput { 25 | action: 'added' | 'deleted' | 'edited'; 26 | member: Member; 27 | repository: Repository; 28 | sender: User; 29 | changes?: { old_permission: { from: string } }; 30 | } 31 | 32 | export class MemberEventModel implements MemberEventInput { 33 | action: 'added' | 'deleted' | 'edited'; 34 | member: Member; 35 | repository: Repository; 36 | sender: User; 37 | changes?: { old_permission: { from: string } }; 38 | 39 | constructor(input: MemberEventInput) { 40 | Object.assign(this, input); 41 | } 42 | 43 | public static isCurrentModel(input: any): boolean { 44 | const requireKeys: string[] = ['action', 'member', 'repository', 'sender']; 45 | 46 | const objKeys: string[] = Object.keys(input); 47 | let length: number = requireKeys.length; 48 | 49 | if (objKeys.find((elem: string) => elem === 'changes')) { 50 | ++length; 51 | } 52 | 53 | return objKeys.length === length && isExistProperties(input, requireKeys); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /functions/src/user/repos.ts: -------------------------------------------------------------------------------- 1 | // Third party modules 2 | import * as firebase from 'firebase-admin'; 3 | 4 | // Dashboard hub firebase functions mappers 5 | import { DocumentReference, FirebaseAdmin, WriteBatch } from './../client/firebase-admin'; 6 | import { GitHubClient } from './../client/github'; 7 | import { Logger } from './../client/logger'; 8 | import { GitHubRepositoryInput, GitHubRepositoryMapper, GitHubRepositoryModel } from './../mappers/github/repository.mapper'; 9 | 10 | export interface ReposInput { 11 | token: string; 12 | } 13 | 14 | export const getUserRepos: any = async (token: string, uid: string) => { 15 | const batch: WriteBatch = FirebaseAdmin.firestore().batch(); 16 | const userRef: DocumentReference = FirebaseAdmin.firestore().collection('users').doc(uid); 17 | let repositories: GitHubRepositoryInput[] = []; 18 | try { 19 | repositories = await GitHubClient('/user/repos?visibility=public&affiliation=owner&sort=updated&per_page=100', token); 20 | } catch (error) { 21 | Logger.error(error); 22 | throw new Error(error); 23 | } 24 | 25 | const mappedRepos: GitHubRepositoryModel[] = repositories.map((repository: GitHubRepositoryInput) => GitHubRepositoryMapper.import(repository)); 26 | 27 | Logger.info({ 28 | user: uid, 29 | imported: { 30 | repos: mappedRepos.length || 0, 31 | }, 32 | }); 33 | 34 | await batch 35 | .update(userRef, { 36 | repositories: { 37 | lastUpdated: firebase.firestore.Timestamp.fromDate(new Date()), 38 | data: mappedRepos, 39 | }, 40 | }); 41 | 42 | await batch.commit(); 43 | 44 | return mappedRepos; 45 | }; 46 | -------------------------------------------------------------------------------- /web/src/app/shared/models/help.model.ts: -------------------------------------------------------------------------------- 1 | import { HelpTopic } from '@shared/models/index.model'; 2 | 3 | export class HelpModel { 4 | icon: string; 5 | title: string; 6 | content: string; 7 | topics: HelpTopic[] = [ 8 | { 9 | title: 'Quickstart', 10 | description: 'Overview information on getting started', 11 | icon: 'emoji_flags', 12 | path: 'quickstart', 13 | updatedAt: '09/13/2019 09:05 AM', 14 | }, 15 | { 16 | title: 'Glossary', 17 | description: 'Coming Soon', 18 | icon: 'menu_book', 19 | path: 'coming-soon', 20 | updatedAt: '09/13/2019 09:05 AM', 21 | }, 22 | { 23 | title: 'Create Project', 24 | description: 'Coming Soon', 25 | icon: 'create_new_folder', 26 | path: 'coming-soon', 27 | updatedAt: '09/13/2019 09:05 AM', 28 | }, 29 | { 30 | title: 'Edit Project', 31 | description: 'Coming Soon', 32 | icon: 'edit', 33 | path: 'coming-soon', 34 | updatedAt: '09/13/2019 09:05 AM', 35 | }, 36 | { 37 | title: 'Delete Project', 38 | description: 'Coming Soon', 39 | icon: 'delete_forever', 40 | path: 'coming-soon', 41 | updatedAt: '09/13/2019 09:05 AM', 42 | }, 43 | { 44 | title: 'How to support us', 45 | description: 'Coming Soon', 46 | icon: 'question_answer', 47 | path: 'coming-soon', 48 | updatedAt: '09/13/2019 09:05 AM', 49 | }, 50 | { 51 | title: 'Why Open Source?', 52 | description: 'Coming Soon', 53 | icon: 'code', 54 | path: 'coming-soon', 55 | updatedAt: '09/13/2019 09:05 AM', 56 | }, 57 | ]; 58 | } 59 | -------------------------------------------------------------------------------- /web/src/app/shared/directives/markdown.directive.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Directive, ElementRef, Input, OnChanges } from '@angular/core'; 3 | 4 | /** 5 | * Markdown directive 6 | */ 7 | @Directive({ 8 | selector: '[dashboardMarkdown]', 9 | exportAs: 'markdown', 10 | }) 11 | export class MarkdownDirective implements OnChanges { 12 | 13 | private _original: string; 14 | 15 | private marked: any = (window).marked; 16 | 17 | @Input() dashboardMarkdown: string; 18 | 19 | /** 20 | * Life cycle method 21 | */ 22 | constructor( 23 | protected element: ElementRef 24 | ) { } 25 | 26 | /** 27 | * Life cycle on changes method 28 | */ 29 | ngOnChanges(): void { 30 | this.marked.setOptions({ 31 | gfm: true, 32 | sanitize: true, 33 | }); 34 | 35 | if (!this._original) { 36 | this._original = this.element.nativeElement.innerHTML; 37 | } 38 | 39 | if (this.dashboardMarkdown) { 40 | this.element.nativeElement.innerHTML = this.marked(this.dashboardMarkdown); 41 | } else { 42 | this.element.nativeElement.innerHTML = this._original; 43 | } 44 | } 45 | 46 | /** 47 | * Render the HTML 48 | */ 49 | materialRender(): any { 50 | 51 | return { 52 | heading: (text: string): string => `

${text}

`, 53 | image: (href: string, title: string, text: string): string => `${text}`, 54 | link: (href: string, title: string, text: string): string => { 55 | return `${text}`; 56 | }, 57 | }; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /web/src/app/core/resolvers/view-project.resolver.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Injectable } from '@angular/core'; 3 | import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; 4 | 5 | // Third party modules 6 | import { AngularFireFunctions } from '@angular/fire/functions'; 7 | import { of, Observable } from 'rxjs'; 8 | import { catchError, take, tap } from 'rxjs/operators'; 9 | 10 | // Dashboard hub model and services 11 | import { ProjectService } from '@core/services/index.service'; 12 | import { IProject, ProjectModel } from '@shared/models/index.model'; 13 | 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class ViewProjectResolver implements Resolve { 18 | 19 | /** 20 | * Life cycle method 21 | * @param fns AngularFireFunctions 22 | * @param projectService ProjectService 23 | * @param router Router 24 | */ 25 | constructor( 26 | private fns: AngularFireFunctions, 27 | private projectService: ProjectService, 28 | private router: Router 29 | ) { } 30 | 31 | /** 32 | * Find all project data before showing projects page 33 | * @param route ActivatedRouteSnapshot 34 | */ 35 | resolve(route: ActivatedRouteSnapshot): Observable { 36 | const callable: any = this.fns.httpsCallable('updateProjectViews'); 37 | 38 | return this.projectService.findOneById(route.params.projectUid) 39 | .pipe( 40 | take(1), 41 | tap(() => callable({ projectUid: route.params.projectUid })), 42 | catchError(() => { 43 | this.router.navigate(['/']); 44 | 45 | return of(new ProjectModel({ uid: 'error' })); 46 | }) 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /web/src/assets/help/quickstart.md: -------------------------------------------------------------------------------- 1 | # Get up and running in minutes in 3 simple steps and without installing anything! 2 | 3 | From the **DashboardHub** Team... 4 | > Thank you for showing an interest in **DashboardHub**. We are currently in an `ALPHA` stage, your feedback would be much appreciated. Feel free to create an [Issue](https://github.com/DashboardHub/PipelineDashboard/issues) on **GitHub** 5 | 6 | ## 1. Log in with GitHub 7 | 8 | Please log in with your GitHub account by clicking on the top right of the page. 9 | 10 | ![Log in](https://user-images.githubusercontent.com/624760/64843082-68531780-d604-11e9-96f1-ab02eaa3aab9.png) 11 | 12 | ## 2. Create a Project 13 | 14 | Once logged click on `Add Project`. `name` and `type` are both required, other fields are optional, however we highly recommend filling them out, you can always update these later on. 15 | 16 | ![Add project](https://user-images.githubusercontent.com/624760/64853131-0d78ea80-d61b-11e9-939d-cfd06c70502b.png) 17 | 18 | Once successfully created, you will be redirected to the **Project Dashboard** page for that project. 19 | 20 | ![Project dashboard page](https://user-images.githubusercontent.com/624760/64853209-39946b80-d61b-11e9-8fe2-553bda3dd272.png) 21 | 22 | ## 3. Add repository 23 | 24 | Click `Repositories` at the top right of the dashboard project, and a dialog will open to select your repositories that you wish to add to the project 25 | 26 | ![Repositories](https://user-images.githubusercontent.com/624760/64853566-ebcc3300-d61b-11e9-9c96-5ed75488596f.png) 27 | 28 | Once you click `confirm`, you will see your project dashboard with the selected repositories *(note: it could take up to 30s to collect all the information for your repositories)* 29 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/watch.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | 3 | // Dashboard mappers/models 4 | import { GitHubEventModel, GitHubEventType } from '../event.mapper'; 5 | import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; 6 | import { GitHubRepositoryMapper } from '../repository.mapper'; 7 | import { GitHubUserMapper } from '../user.mapper'; 8 | import { isExistProperties, HubEventActions, Repository, User } from './shared'; 9 | 10 | export interface WatchEventInput { 11 | action: 'started'; 12 | repository: Repository; 13 | sender: User; 14 | } 15 | 16 | export class WatchEventModel implements WatchEventInput, HubEventActions { 17 | action: 'started'; 18 | repository: Repository; 19 | sender: User; 20 | 21 | constructor(input: WatchEventInput) { 22 | Object.assign(this, input); 23 | } 24 | 25 | public static isCurrentModel(input: any): boolean { 26 | const requireKeys: string[] = ['action', 'repository', 'sender']; 27 | return Object.keys(input).length === requireKeys.length && isExistProperties(input, requireKeys) && input.action === 'started'; 28 | } 29 | 30 | convertToHubEvent(): GitHubEventModel { 31 | const eventType: GitHubEventType = 'WatchEvent'; 32 | const payload: GitHubPayloadInput = { 33 | action: this.action, 34 | } 35 | 36 | const data: GitHubEventModel = { 37 | type: eventType, 38 | public: true, // TODO where get 39 | actor: GitHubUserMapper.import(this.sender), 40 | repository: GitHubRepositoryMapper.import(this.repository, 'event'), 41 | payload: GitHubPayloadMapper.import(eventType, payload), 42 | createdOn: firestore.Timestamp.now(), 43 | }; 44 | 45 | return data; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionGroup": "projects", 5 | "queryScope": "COLLECTION", 6 | "fields": [ 7 | { 8 | "fieldPath": "type", 9 | "mode": "ASCENDING" 10 | }, 11 | { 12 | "fieldPath": "updatedOn", 13 | "mode": "DESCENDING" 14 | } 15 | ] 16 | }, 17 | { 18 | "collectionGroup": "projects", 19 | "queryScope": "COLLECTION", 20 | "fields": [ 21 | { 22 | "fieldPath": "access.admin", 23 | "mode": "ASCENDING" 24 | }, 25 | { 26 | "fieldPath": "updatedOn", 27 | "mode": "DESCENDING" 28 | } 29 | ] 30 | }, 31 | { 32 | "collectionGroup": "projects", 33 | "queryScope": "COLLECTION", 34 | "fields": [ 35 | { 36 | "fieldPath": "type", 37 | "mode": "ASCENDING" 38 | }, 39 | { 40 | "fieldPath": "views", 41 | "mode": "DESCENDING" 42 | } 43 | ] 44 | }, 45 | { 46 | "collectionGroup": "projects", 47 | "queryScope": "COLLECTION", 48 | "fields": [ 49 | { 50 | "fieldPath": "access.admin", 51 | "mode": "ARRAY_CONTAINS" 52 | }, 53 | { 54 | "fieldPath": "updatedOn", 55 | "mode": "DESCENDING" 56 | } 57 | ] 58 | }, 59 | { 60 | "collectionGroup": "pings", 61 | "queryScope": "COLLECTION_GROUP", 62 | "fields": [ 63 | { 64 | "fieldPath": "monitorUid", 65 | "mode": "DESCENDING" 66 | }, 67 | { 68 | "fieldPath": "createdOn", 69 | "mode": "DESCENDING" 70 | } 71 | ] 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /functions/src/mappers/github/payload.mapper.ts: -------------------------------------------------------------------------------- 1 | import { GitHubEventType } from './event.mapper'; 2 | 3 | export interface GitHubPayloadInput { 4 | title?: string; 5 | action?: string; 6 | ref?: string; 7 | ref_type?: string; 8 | pull_request?: { 9 | title: string; 10 | }; 11 | issue?: { 12 | title: string; 13 | }; 14 | comment?: { 15 | body: string; 16 | }; 17 | release?: { 18 | tag_name: string; 19 | target_commitish: string; 20 | name?: string; 21 | }; 22 | } 23 | 24 | export interface GitHubPayloadModel { 25 | title: string; 26 | action?: string; 27 | } 28 | 29 | export class GitHubPayloadMapper { 30 | static import( 31 | type: GitHubEventType, 32 | input: GitHubPayloadInput 33 | ): GitHubPayloadModel { 34 | const output: any = {}; 35 | 36 | switch (type) { 37 | case 'PullRequestEvent': 38 | output.title = input.pull_request.title; 39 | break; 40 | case 'IssuesEvent': 41 | output.title = input.issue.title; 42 | output.action = input.action; 43 | break; 44 | case 'IssueCommentEvent': 45 | output.title = input.comment.body; 46 | output.action = input.action; 47 | break; 48 | case 'CreateEvent': 49 | output.title = `${input.ref_type}: ${input.ref}`; 50 | break; 51 | case 'ReleaseEvent': 52 | output.title = `${input.action}: ${input.release.tag_name}@${input.release.target_commitish}` + (input.release.name ? ` - ${input.release.name}` : ''); 53 | break; 54 | case 'WatchEvent': 55 | output.title = `${input.action} watching`; 56 | break; 57 | case 'PushEvent': 58 | output.title = `Push to ${input.ref} branch`; 59 | break; 60 | } 61 | 62 | return output; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /web/test/integration/homepage/homepage.feature: -------------------------------------------------------------------------------- 1 | Feature: Open the website landing page 2 | 3 | Scenario: Ensure the page has loaded correctly 4 | Given the "/" page is open 5 | Then the title on the page says "PipelineDashboard" 6 | And the text "Homepage" is in the element ".home__title" 7 | And the text "Welcome Guest" is in the element ".app-toolbar" 8 | And the text "Application statistics" is in the element ".stats__card__title" 9 | And the text "Active Users" is in the element ".home__users-card__title" 10 | And the text "Popular projects" is in the element ".home__users-card__title" 11 | And the text "Public Projects" is in the element ".project-card__title" 12 | 13 | Scenario: Ensure the application statistics for users 14 | Given there is a document "stats" with the field "users" set to 100 in collection "platform" 15 | And the "/" page is open 16 | Then the count "100" is in the element ".stats__card__content__item__red" 17 | 18 | Scenario: Ensure the application statistics for project 19 | Given there is a document "stats" with the field "projects" set to 1195 in collection "platform" 20 | And the "/" page is open 21 | Then the count "1.2k" is in the element ".stats__card__content__item__blue" 22 | 23 | Scenario: Ensure the application statistics for pings 24 | Given there is a document "stats" with the field "pings" set to 16123 in collection "platform" 25 | And the "/" page is open 26 | Then the count "16.1k" is in the element ".stats__card__content__item__purple" 27 | 28 | Scenario: Ensure the application statistics for events 29 | Given there is a document "stats" with the field "events" set to 101000 in collection "platform" 30 | And the "/" page is open 31 | Then the count "101k" is in the element ".stats__card__content__item__dark-blue" 32 | -------------------------------------------------------------------------------- /web/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { CommonModule } from '@angular/common'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { NgModule } from '@angular/core'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { RouterModule } from '@angular/router'; 7 | import { environment } from '../environments/environment'; 8 | 9 | // Third party modules 10 | import { AngularFireModule } from '@angular/fire'; 11 | import { AngularFireAuthModule } from '@angular/fire/auth'; 12 | import { AngularFirestoreModule } from '@angular/fire/firestore'; 13 | import { AngularFireFunctionsModule, FunctionsRegionToken } from '@angular/fire/functions'; 14 | import { DeviceDetectorModule } from 'ngx-device-detector'; 15 | 16 | // Dashboard hub modules 17 | import { AppRoutingModule } from './app-routing.module'; 18 | import { CoreModule } from './core/core.module'; 19 | import { SharedModule } from './shared/shared.module'; 20 | 21 | // Dashboard hub components 22 | import { AppComponent } from './app.component'; 23 | 24 | export function tokenGetter(): string | null { 25 | return localStorage.getItem('access_token'); 26 | } 27 | 28 | @NgModule({ 29 | declarations: [ 30 | AppComponent, 31 | ], 32 | imports: [ 33 | AngularFireAuthModule, 34 | AngularFireModule.initializeApp(environment.firebase), 35 | AngularFirestoreModule.enablePersistence(), 36 | AngularFireFunctionsModule, 37 | BrowserAnimationsModule, 38 | CommonModule, 39 | DeviceDetectorModule.forRoot(), 40 | HttpClientModule, 41 | RouterModule, 42 | AppRoutingModule, 43 | CoreModule, 44 | SharedModule, 45 | ], 46 | providers: [ 47 | { provide: FunctionsRegionToken, useValue: 'us-central1' }, 48 | ], 49 | bootstrap: [AppComponent], 50 | }) 51 | export class AppModule { 52 | } 53 | -------------------------------------------------------------------------------- /web/src/app/core/resolvers/edit-project.resolver.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Injectable } from '@angular/core'; 3 | import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; 4 | import { of, Observable } from 'rxjs'; 5 | import { catchError, switchMap, take } from 'rxjs/operators'; 6 | 7 | // Dashboard hub model and services 8 | import { AuthenticationService, ProjectService } from '@core/services/index.service'; 9 | import { IProject, ProjectModel } from '@shared/models/index.model'; 10 | 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class EditProjectResolver implements Resolve { 15 | 16 | /** 17 | * Life cycle method 18 | * @param authService AuthenticationService 19 | * @param projectService ProjectService 20 | * @param router Router 21 | */ 22 | constructor( 23 | private authService: AuthenticationService, 24 | private projectService: ProjectService, 25 | private router: Router 26 | ) { } 27 | 28 | /** 29 | * Allow owner to edit project routes only 30 | * @param route ActivatedRouteSnapshot 31 | */ 32 | resolve(route: ActivatedRouteSnapshot): Observable { 33 | return this.projectService.findOneById(route.params.projectUid) 34 | .pipe( 35 | take(1), 36 | switchMap((project: ProjectModel) => { 37 | // for private project must have access 38 | if (!project || (project.isPrivate() && !project.isAdmin(this.authService.profile.uid))) { 39 | this.router.navigate(['/projects']); 40 | 41 | return of(new ProjectModel({ uid: 'error'})); 42 | } 43 | 44 | return of(project); 45 | }), 46 | catchError(() => { 47 | this.router.navigate(['/projects']); 48 | 49 | return of(new ProjectModel({ uid: 'error'})); 50 | }) 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /functions/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | 2 | export const enviroment: Config = { 3 | githubWebhook: { 4 | url: 'https://{{ FIREBASE_FUNCTIONS_URL }}.cloudfunctions.net/responseGitWebhookRepository', 5 | secret: '{{ GITHUB_WEBHOOK_SECRET }}', 6 | content_type: 'json', 7 | insecure_ssl: '0', 8 | events: [ 9 | // IMPLEMENTED 10 | 'create', 11 | 'issue_comment', 12 | 'issues', 13 | 'member', 14 | 'milestone', 15 | 'pull_request', 16 | 'push', 17 | 'release', 18 | 'repository', 19 | 'status', 20 | 'watch', 21 | 22 | // NOT IMPLEMENTED 23 | 'check_run', 24 | 'check_suite', 25 | 'commit_comment', 26 | 'delete', 27 | 'deploy_key', 28 | 'deployment', 29 | 'deployment_status', 30 | 'fork', 31 | 'gollum', 32 | 'label', 33 | 'meta', 34 | 'page_build', 35 | 'project_card', 36 | 'project_column', 37 | 'project', 38 | 'public', 39 | 'pull_request_review', 40 | 'pull_request_review_comment', 41 | 'registry_package', 42 | 'repository_import', 43 | 'repository_vulnerability_alert', 44 | 'star', 45 | 'team_add', 46 | 47 | // NOT ALLOW for this hook 48 | // 'content_reference', 49 | // 'github_app_authorization', 50 | // 'installation', 51 | // 'installation_repositories', 52 | // 'marketplace_purchase', 53 | // 'membership', 54 | // 'organization', 55 | // 'org_block', 56 | // 'repository_dispatch', 57 | // 'security_advisory', 58 | // 'team', 59 | ], 60 | }, 61 | 62 | } 63 | 64 | interface Config { 65 | githubWebhook: { 66 | url: string, 67 | secret: string, 68 | content_type: 'json' | 'form', 69 | insecure_ssl: '0' | '1', 70 | events: string[], 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /web/src/app/app-material.module.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { 5 | MatBadgeModule, 6 | MatButtonModule, 7 | MatCardModule, 8 | MatDialogModule, 9 | MatDividerModule, 10 | MatExpansionModule, 11 | MatIconModule, 12 | MatInputModule, 13 | MatListModule, 14 | MatMenuModule, 15 | MatProgressBarModule, 16 | MatProgressSpinnerModule, 17 | MatRadioModule, 18 | MatSelectModule, 19 | MatSidenavModule, 20 | MatSlideToggleModule, 21 | MatSnackBarModule, 22 | MatTableModule, 23 | MatTabsModule, 24 | MatToolbarModule, 25 | MatTooltipModule, 26 | } from '@angular/material'; 27 | 28 | @NgModule({ 29 | imports: [ 30 | MatBadgeModule, 31 | MatButtonModule, 32 | MatCardModule, 33 | MatDialogModule, 34 | MatDividerModule, 35 | MatIconModule, 36 | MatInputModule, 37 | MatListModule, 38 | MatProgressBarModule, 39 | MatProgressSpinnerModule, 40 | MatRadioModule, 41 | MatSelectModule, 42 | MatSidenavModule, 43 | MatSnackBarModule, 44 | MatTabsModule, 45 | MatToolbarModule, 46 | MatTooltipModule, 47 | MatExpansionModule, 48 | MatMenuModule, 49 | MatTableModule, 50 | MatSlideToggleModule, 51 | ], 52 | exports: [ 53 | MatBadgeModule, 54 | MatButtonModule, 55 | MatCardModule, 56 | MatDialogModule, 57 | MatDividerModule, 58 | MatIconModule, 59 | MatInputModule, 60 | MatListModule, 61 | MatProgressBarModule, 62 | MatProgressSpinnerModule, 63 | MatRadioModule, 64 | MatSelectModule, 65 | MatSidenavModule, 66 | MatSnackBarModule, 67 | MatTabsModule, 68 | MatToolbarModule, 69 | MatTooltipModule, 70 | MatExpansionModule, 71 | MatMenuModule, 72 | MatTableModule, 73 | MatSlideToggleModule, 74 | ], 75 | }) 76 | export class AppMaterialModule { 77 | } 78 | -------------------------------------------------------------------------------- /functions/src/user/create-user.ts: -------------------------------------------------------------------------------- 1 | // Third party modules 2 | import { firestore, CloudFunction, EventContext } from 'firebase-functions'; 3 | import { v4 as uuid } from 'uuid'; 4 | 5 | // Dashboard hub firebase functions models/mappers 6 | import { Logger } from '../client/logger'; 7 | import { GitHubRepositoryModel } from '../mappers/github/index.mapper'; 8 | import { DocumentData, DocumentSnapshot } from './../client/firebase-admin'; 9 | 10 | // Dashboard models 11 | import { RepositoryModel } from '../models/index.model'; 12 | 13 | async function addUidToRepositories(user: DocumentData): Promise { 14 | if (user.repositories && Array.isArray(user.repositories.data) && user.repositories.data.length > 0) { 15 | const uids: string[] = []; 16 | user.repositories.data = user.repositories.data 17 | .filter((item: GitHubRepositoryModel) => item && item.id !== null && item.id !== undefined); 18 | 19 | for (const item of user.repositories.data) { 20 | const exitsRepoUid: string = await RepositoryModel.getRepositoryUidById(item.id); 21 | if (exitsRepoUid) { 22 | item.uid = exitsRepoUid; 23 | } else { 24 | item.uid = uuid(); 25 | } 26 | uids.push(item.uid); 27 | } 28 | user.repositories.data.uids = uids 29 | return true; 30 | } 31 | 32 | return false; 33 | } 34 | 35 | export const onCreateUser: CloudFunction = firestore 36 | .document('users/{userUid}') 37 | .onCreate(async (userSnapshot: DocumentSnapshot, context: EventContext) => { 38 | try { 39 | const newData: DocumentData = userSnapshot.data(); 40 | const isNeedUpdate: boolean = await addUidToRepositories(newData); 41 | if (isNeedUpdate) { 42 | await userSnapshot.ref.update(newData); 43 | } 44 | } catch (err) { 45 | Logger.error(err); 46 | throw new Error(err); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /web/src/app/shared/components/breadcrumb/breadcrumb.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component, Input, OnInit } from '@angular/core'; 3 | 4 | // Breakpoints components 5 | import { Breakpoints, BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; 6 | 7 | // DashboardHub Models 8 | import { AuthenticationService } from '@core/services/index.service'; 9 | import { BreadCrumbModel, ProjectModel } from '@shared/models/index.model'; 10 | 11 | /** 12 | * Breadcrumb component 13 | */ 14 | @Component({ 15 | selector: 'dashboard-breadcrumb', 16 | templateUrl: './breadcrumb.component.html', 17 | styleUrls: ['./breadcrumb.component.scss'], 18 | }) 19 | export class BreadcrumbComponent implements OnInit { 20 | 21 | public typeIcon: string; 22 | public isSmallScreen: Boolean; 23 | 24 | @Input() project: ProjectModel; 25 | @Input() subTitle: string; 26 | @Input() breadCrumb: BreadCrumbModel[]; 27 | 28 | /** 29 | * Life cycle method 30 | * @param breakpointObserver BreakpointObserver 31 | * @param authService AuthenticationService 32 | */ 33 | constructor( 34 | private breakpointObserver: BreakpointObserver, 35 | private authService: AuthenticationService 36 | ) { } 37 | 38 | /** 39 | * Life cycle init method 40 | */ 41 | ngOnInit(): void { 42 | this.typeIcon = this.project.isPrivate() ? 'lock' : 'lock_open'; 43 | this.breakpointObserver 44 | .observe([Breakpoints.XSmall]) 45 | .subscribe((state: BreakpointState) => { 46 | if (state.matches) { 47 | this.isSmallScreen = true; 48 | } else { 49 | this.isSmallScreen = false; 50 | } 51 | }); 52 | } 53 | 54 | /** 55 | * Check if logged in user is also owner of the project 56 | */ 57 | public isAdmin(): boolean { 58 | return this.project.isAdmin(this.authService.profile.uid); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /functions/src/mappers/github/release.mapper.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | 3 | // Dashboard mappers/models 4 | import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './user.mapper'; 5 | 6 | export interface GitHubReleaseInput { 7 | id: number; 8 | name: string; 9 | body: string; 10 | author: GitHubUserInput; 11 | html_url: string; 12 | published_at: string; 13 | prerelease: boolean; 14 | } 15 | 16 | export interface GitHubReleaseModel { 17 | uid: number; 18 | title: string; 19 | description: string; 20 | owner: GitHubUserModel; 21 | htmlUrl: string; 22 | createdOn: firestore.Timestamp; 23 | isPrerelease: boolean; 24 | } 25 | 26 | export class GitHubReleaseMapper { 27 | static import(input: GitHubReleaseInput): GitHubReleaseModel { 28 | return { 29 | uid: input.id, 30 | title: input.name, 31 | description: input.body, 32 | owner: GitHubUserMapper.import(input.author), 33 | htmlUrl: input.html_url, 34 | createdOn: firestore.Timestamp.fromDate(new Date(input.published_at)), 35 | isPrerelease: input.prerelease, 36 | }; 37 | } 38 | 39 | public static sortReleaseList(releases: GitHubReleaseModel[]): GitHubReleaseModel[] { 40 | return releases 41 | .sort( 42 | (a: GitHubReleaseModel, b: GitHubReleaseModel): number => { 43 | // tslint:disable-next-line: triple-equals 44 | if (a.createdOn == null && b.createdOn == null) { 45 | return 0; 46 | } 47 | // tslint:disable-next-line: triple-equals 48 | if (a.createdOn == null) { 49 | return 1; 50 | } 51 | // tslint:disable-next-line: triple-equals 52 | if (b.createdOn == null) { 53 | return -1; 54 | } 55 | return b.createdOn.toMillis() - a.createdOn.toMillis(); 56 | } 57 | ) 58 | ; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /web/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | // Third party module 5 | import { FlexLayoutModule } from '@angular/flex-layout'; 6 | 7 | // Dashboard hub App modules 8 | import { ReactiveFormsModule } from '@angular/forms'; 9 | import { RouterModule } from '@angular/router'; 10 | import { AppMaterialModule } from '../app-material.module'; 11 | import { PipesModule } from '../pipes/pipes.module'; 12 | 13 | // Dashboard hub components 14 | import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component'; 15 | import { PrivatePublicProjectComponent } from './components/private-public-project/private-public-project.component'; 16 | import { ProjectsListComponent } from './components/projects-list/projects-list.component'; 17 | import { DialogConfirmationComponent } from './dialog/confirmation/dialog-confirmation.component'; 18 | import { DialogListComponent } from './dialog/list/dialog-list.component'; 19 | import { MarkdownDirective } from './directives/markdown.directive'; 20 | 21 | @NgModule({ 22 | declarations: [ 23 | MarkdownDirective, 24 | DialogConfirmationComponent, 25 | DialogListComponent, 26 | ProjectsListComponent, 27 | PrivatePublicProjectComponent, 28 | BreadcrumbComponent, 29 | ], 30 | imports: [ 31 | CommonModule, 32 | FlexLayoutModule, 33 | AppMaterialModule, 34 | RouterModule.forChild([]), 35 | ReactiveFormsModule, 36 | PipesModule, 37 | ], 38 | exports: [ 39 | FlexLayoutModule, 40 | AppMaterialModule, 41 | ReactiveFormsModule, 42 | PipesModule, 43 | MarkdownDirective, 44 | RouterModule, 45 | BreadcrumbComponent, 46 | ProjectsListComponent, 47 | PrivatePublicProjectComponent, 48 | ], 49 | entryComponents: [ 50 | DialogConfirmationComponent, 51 | DialogListComponent, 52 | ], 53 | }) 54 | export class SharedModule { } 55 | -------------------------------------------------------------------------------- /web/src/app/pipes/time-ago.pipe.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Pipe, PipeTransform } from '@angular/core'; 3 | 4 | /** 5 | * Time transform pipe 6 | */ 7 | @Pipe({ name: 'timeAgo' }) 8 | export class TimeAgoPipe implements PipeTransform { 9 | transform(referenceDate: Date): string { 10 | referenceDate = new Date(referenceDate); 11 | 12 | if (!referenceDate.getTime()) { 13 | return 'Invalid Date'; 14 | } 15 | 16 | const date: Date = new Date(); 17 | let lapse: number = Math.floor((date.getTime() - referenceDate.getTime()) / 1000); 18 | 19 | // Seconds 20 | if (lapse < 2) { 21 | return '1 second ago'; 22 | } 23 | 24 | if (lapse < 60) { 25 | return Math.floor(lapse) + ' seconds ago'; 26 | } 27 | 28 | // Minutes 29 | lapse /= 60; 30 | if (lapse < 2) { 31 | return '1 minute ago'; 32 | } 33 | 34 | if (lapse < 60) { 35 | return Math.floor(lapse) + ' minutes ago'; 36 | } 37 | 38 | // Hours 39 | lapse /= 60; 40 | if (lapse < 2) { 41 | return '1 hour ago'; 42 | } 43 | 44 | if (lapse < 24) { 45 | return Math.floor(lapse) + ' hours ago'; 46 | } 47 | 48 | // Days 49 | lapse /= 24; 50 | if (lapse < 2) { 51 | return '1 day ago'; 52 | } 53 | 54 | if (lapse < 30) { 55 | return Math.floor(lapse) + ' days ago'; 56 | } 57 | 58 | // Weeks 59 | lapse /= 7; 60 | if (lapse < 2) { 61 | return '1 week ago'; 62 | } 63 | 64 | if (lapse < 4.3) { 65 | return Math.floor(lapse) + ' weeks ago'; 66 | } 67 | 68 | // Months 69 | lapse /= 4.3; 70 | if (lapse < 2) { 71 | return '1 month ago'; 72 | } 73 | 74 | if (lapse < 12) { 75 | return Math.floor(lapse) + ' months ago'; 76 | } 77 | 78 | // Years 79 | lapse /= 12; 80 | if (lapse < 2) { 81 | return '1 year ago'; 82 | } 83 | 84 | return Math.floor(lapse) + ' years ago'; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /web/src/app/projects/rating/rating.component.ts: -------------------------------------------------------------------------------- 1 | // Core components 2 | import { Component, OnDestroy, OnInit } from '@angular/core'; 3 | 4 | // Application services/model 5 | import { ActivatedRoute } from '@angular/router'; 6 | import { BreadCrumbModel, ProjectModel, RepositoryModel } from '@app/shared/models/index.model'; 7 | import { RepositoryService } from '@core/services/index.service'; 8 | import { Subscription } from 'rxjs'; 9 | 10 | /** 11 | * Repository rating component 12 | */ 13 | @Component({ 14 | selector: 'dashboard-rating', 15 | templateUrl: './rating.component.html', 16 | styleUrls: ['./rating.component.scss'], 17 | }) 18 | export class RatingComponent implements OnInit, OnDestroy { 19 | 20 | private repositorySubscription: Subscription; 21 | 22 | public project: ProjectModel; 23 | public repository: RepositoryModel; 24 | public breadCrumb: BreadCrumbModel[]; 25 | 26 | /** 27 | * Life cycle method 28 | * @param repositoryService Repository service 29 | * @param route ActivatedRoute 30 | */ 31 | constructor( 32 | private repositoryService: RepositoryService, 33 | private route: ActivatedRoute 34 | ) { 35 | this.route.data.subscribe((data: { repository: RepositoryModel, project: ProjectModel }) => { 36 | this.repository = data.repository; 37 | this.project = data.project; 38 | this.breadCrumb = [{ link: `/projects/${this.project.uid}`, title: this.project.title }]; 39 | }); 40 | } 41 | 42 | /** 43 | * Life cycle init method 44 | */ 45 | ngOnInit(): void { 46 | const repoUid: string = this.route.snapshot.paramMap.get('repoUid'); 47 | 48 | this.repositorySubscription = this.repositoryService 49 | .findOneById(repoUid) 50 | .subscribe((repository: RepositoryModel) => this.repository = repository); 51 | } 52 | 53 | /** 54 | * Life cycle destroy method 55 | */ 56 | ngOnDestroy(): void { 57 | this.repositorySubscription.unsubscribe(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /functions/src/scheduler/schedule.ts: -------------------------------------------------------------------------------- 1 | // 3rd party 2 | import { firestore } from 'firebase-admin'; 3 | import * as functions from 'firebase-functions'; 4 | import { FirebaseAdmin } from '../client/firebase-admin'; 5 | 6 | // Dashboard Hub models 7 | import { Logger } from '../client/logger'; 8 | import { MonitorModel, ProjectModel } from '../models/index.model'; 9 | import { ping } from '../monitor/monitor'; 10 | import { deleteProjectPings } from '../project/delete-project'; 11 | 12 | type QuerySnapshot = firestore.QuerySnapshot; 13 | type WriteResult = firestore.WriteResult; 14 | 15 | export const deletePingsAfter30days: any = functions.pubsub.schedule('00 00 * * *').timeZone('UTC') 16 | .onRun(async () => { 17 | const snapshots: QuerySnapshot = await FirebaseAdmin.firestore() 18 | .collection('projects') 19 | .get(); 20 | 21 | const promises: Promise[] = []; 22 | 23 | snapshots.docs.forEach((doc: firestore.QueryDocumentSnapshot) => { 24 | promises.push(deleteProjectPings(doc.data(), true)); 25 | }); 26 | return Promise.all(promises); 27 | }); 28 | 29 | export const runAllMonitors60Mins: any = functions.pubsub.schedule('every 60 mins') 30 | .onRun(async () => { 31 | const snapshots: QuerySnapshot = await FirebaseAdmin.firestore() 32 | .collection('projects') 33 | .get(); 34 | 35 | const promises: Promise[] = []; 36 | let project: ProjectModel; 37 | snapshots.docs.forEach(async (doc: firestore.QueryDocumentSnapshot) => { 38 | project = doc.data(); 39 | try { 40 | if (project.url && Array.isArray(project.monitors) && project.monitors.length > 0) { 41 | project.monitors.forEach((monitor: MonitorModel) => promises.push(ping(project.uid, monitor.uid))); 42 | } 43 | } catch (err) { 44 | Logger.error(err); 45 | throw new Error(err); 46 | } 47 | }); 48 | return Promise.all(promises); 49 | }); 50 | -------------------------------------------------------------------------------- /web/src/app/core/interceptors/error.http.interceptor.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HTTP_INTERCEPTORS } from '@angular/common/http'; 3 | import { Injectable, NgModule } from '@angular/core'; 4 | import { MatSnackBar } from '@angular/material'; 5 | import { Router } from '@angular/router'; 6 | 7 | // Rxjs operators 8 | import { Observable } from 'rxjs'; 9 | import { tap } from 'rxjs/operators'; 10 | 11 | @Injectable() 12 | export class ErrorHttpInterceptor implements HttpInterceptor { 13 | 14 | /** 15 | * Life cycle method 16 | * @param router Router 17 | * @param snackBar Snackbar 18 | */ 19 | constructor( 20 | private router: Router, 21 | private snackBar: MatSnackBar 22 | ) { } 23 | 24 | /** 25 | * Showing error in request 26 | * @param req HttpRequest 27 | * @param next HttpHandler 28 | */ 29 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 30 | return next.handle(req).pipe(tap(undefined, (error: HttpErrorResponse) => { 31 | if (error instanceof HttpErrorResponse) { 32 | let message: string = ''; 33 | switch (error.status) { 34 | case 401: 35 | case 403: 36 | message = 'Permission denied. Please try again.'; 37 | break; 38 | case 404: 39 | message = 'Not found. Please try again.'; 40 | break; 41 | default: 42 | this.snackBar.open(error.message, undefined, { duration: 5000 }); 43 | break; 44 | } 45 | 46 | this.router.navigate(['/']) 47 | .then(() => this.snackBar.open(message, undefined, { duration: 5000 })); 48 | } 49 | })); 50 | } 51 | } 52 | 53 | @NgModule({ 54 | providers: [ 55 | { 56 | provide: HTTP_INTERCEPTORS, 57 | useClass: ErrorHttpInterceptor, 58 | multi: true, 59 | }, 60 | ], 61 | }) 62 | export class ErrorHttpInterceptorModule { 63 | } 64 | -------------------------------------------------------------------------------- /functions/src/mappers/github/event.mapper.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | 3 | // Dashboard mappers/models 4 | import { GitHubEventType } from './event.mapper'; 5 | import { GitHubOrganisationtInput, GitHubOrganisationMapper, GitHubOrganisationModel } from './organisation.mapper'; 6 | import { GitHubPayloadInput, GitHubPayloadMapper, GitHubPayloadModel } from './payload.mapper'; 7 | import { GitHubRepositoryInput, GitHubRepositoryMapper, GitHubRepositoryModel } from './repository.mapper'; 8 | import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './user.mapper'; 9 | 10 | export type GitHubEventType = 'PullRequestEvent' | 'IssueCommentEvent' | 'CreateEvent' | 'ReleaseEvent' | 'WatchEvent' | 'PushEvent' | 'IssuesEvent'; 11 | 12 | export interface GitHubEventInput { 13 | id: string; 14 | type: GitHubEventType; 15 | public: boolean; 16 | actor: GitHubUserInput; 17 | repo: GitHubRepositoryInput; 18 | org: GitHubOrganisationtInput; 19 | payload: GitHubPayloadInput; 20 | created_at: string; 21 | } 22 | 23 | export interface GitHubEventModel { 24 | uid?: string; 25 | type: GitHubEventType; 26 | public: boolean; 27 | actor: GitHubUserModel; 28 | repository: GitHubRepositoryModel; 29 | organisation?: GitHubOrganisationModel; 30 | payload: GitHubPayloadModel; 31 | createdOn: firestore.Timestamp; 32 | } 33 | 34 | export class GitHubEventMapper { 35 | static import(input: GitHubEventInput): GitHubEventModel { 36 | const data: GitHubEventModel = { 37 | uid: input.id, 38 | type: input.type, 39 | public: input.public, 40 | actor: GitHubUserMapper.import(input.actor), 41 | repository: GitHubRepositoryMapper.import(input.repo, 'event'), 42 | payload: GitHubPayloadMapper.import(input.type, input.payload), 43 | createdOn: firestore.Timestamp.fromDate(new Date(input.created_at)), 44 | }; 45 | 46 | if (input.org) { 47 | data.organisation = GitHubOrganisationMapper.import(input.org); 48 | } 49 | 50 | return data; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /web/src/app/main/components/help/help.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Help
4 |
5 | 6 | 7 |
8 |
9 | 10 | search 11 | 12 | Search 13 | 14 | 15 | 16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | {{ help.icon }} 24 |
25 |
{{ help.title }}
26 |
{{ help.description }}
27 |
28 | 29 | 30 |
31 | update 32 |
33 |
{{ help.updatedAt | timeAgo }}
34 |
35 |
36 | 37 |
38 |
39 |

No topics to display.

40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /web/src/app/main/components/help/help.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Component, OnInit } from '@angular/core'; 4 | import { FormBuilder, FormGroup } from '@angular/forms'; 5 | 6 | // DashboardHub Models 7 | import { HelpModel, HelpTopic } from '@shared/models/index.model'; 8 | 9 | /** 10 | * Help component 11 | */ 12 | @Component({ 13 | selector: 'dashboard-help', 14 | templateUrl: './help.component.html', 15 | styleUrls: ['./help.component.scss'], 16 | }) 17 | export class HelpComponent implements OnInit { 18 | 19 | public searchForm: FormGroup; 20 | public filteredTopics: HelpTopic[] = []; 21 | public help: HelpModel = new HelpModel(); 22 | public topics: HelpTopic[]; 23 | 24 | /** 25 | * Life cycle method 26 | * @param http HttpClient 27 | * @param form FormBuilder 28 | * @param dialog MatDialog 29 | */ 30 | constructor( 31 | private http: HttpClient, 32 | private form: FormBuilder 33 | ) { } 34 | 35 | /** 36 | * Life cycle init method 37 | */ 38 | ngOnInit(): void { 39 | this.topics = this.help.topics; 40 | this.topics.forEach((help: { title: string, path: string }, index: number) => { 41 | this.http.get(`/assets/help/${help.path}.md`, { responseType: 'text' }).subscribe((content: string) => { 42 | this.topics[index].content = content; 43 | }); 44 | }); 45 | this.filteredTopics = this.topics; 46 | 47 | this.searchForm = this.form.group({ 48 | search: [undefined, []], 49 | }); 50 | 51 | this.searchForm.get('search').valueChanges.subscribe((search: string) => this.filterTopics(search)); 52 | } 53 | 54 | /** 55 | * Searches the topics in help page 56 | * @param keyword key to search in help page 57 | */ 58 | filterTopics(keyword: string = ''): void { 59 | this.filteredTopics = this.topics.filter((help: HelpTopic) => { 60 | return help.title.toLowerCase().includes(keyword.toLowerCase()) || help.description.toLowerCase().includes(keyword.toLowerCase()); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/shared/repository.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user'; 2 | 3 | export interface Repository { 4 | id: number; 5 | node_id: string; 6 | name: string; 7 | full_name: string; 8 | private: boolean; 9 | owner: User; 10 | html_url: string; 11 | description?: any; 12 | fork: boolean; 13 | url: string; 14 | forks_url: string; 15 | keys_url: string; 16 | collaborators_url: string; 17 | teams_url: string; 18 | hooks_url: string; 19 | issue_events_url: string; 20 | events_url: string; 21 | assignees_url: string; 22 | branches_url: string; 23 | tags_url: string; 24 | blobs_url: string; 25 | git_tags_url: string; 26 | git_refs_url: string; 27 | trees_url: string; 28 | statuses_url: string; 29 | languages_url: string; 30 | stargazers_url: string; 31 | contributors_url: string; 32 | subscribers_url: string; 33 | subscription_url: string; 34 | commits_url: string; 35 | git_commits_url: string; 36 | comments_url: string; 37 | issue_comment_url: string; 38 | contents_url: string; 39 | compare_url: string; 40 | merges_url: string; 41 | archive_url: string; 42 | downloads_url: string; 43 | issues_url: string; 44 | pulls_url: string; 45 | milestones_url: string; 46 | notifications_url: string; 47 | labels_url: string; 48 | releases_url: string; 49 | deployments_url: string; 50 | created_at: Date; 51 | updated_at: Date; 52 | pushed_at: Date; 53 | git_url: string; 54 | ssh_url: string; 55 | clone_url: string; 56 | svn_url: string; 57 | homepage?: any; 58 | size: number; 59 | stargazers_count: number; 60 | watchers_count: number; 61 | language?: string; 62 | has_issues: boolean; 63 | has_projects: boolean; 64 | has_downloads: boolean; 65 | has_wiki: boolean; 66 | has_pages: boolean; 67 | forks_count: number; 68 | mirror_url?: any; 69 | archived: boolean; 70 | disabled: boolean; 71 | open_issues_count: number; 72 | license?: any; 73 | forks: number; 74 | open_issues: number; 75 | watchers: number; 76 | default_branch: string; 77 | } 78 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/create.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | 3 | // Dashboard mappers/models 4 | import { GitHubEventModel, GitHubEventType } from '../event.mapper'; 5 | import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; 6 | import { GitHubRepositoryMapper } from '../repository.mapper'; 7 | import { GitHubUserMapper } from '../user.mapper'; 8 | import { isExistProperties, HubEventActions, Repository, User } from './shared'; 9 | 10 | export interface CreateEventInput { 11 | ref: string; 12 | ref_type: 'branch' | 'tag'; 13 | master_branch: string; 14 | description?: any; 15 | pusher_type: string; 16 | repository: Repository; 17 | sender: User; 18 | } 19 | 20 | export class CreateEventModel implements CreateEventInput, HubEventActions { 21 | ref: string; 22 | ref_type: 'branch' | 'tag'; 23 | master_branch: string; 24 | description?: any; 25 | pusher_type: string; 26 | repository: Repository; 27 | sender: User; 28 | 29 | constructor(input: CreateEventInput) { 30 | Object.assign(this, input); 31 | } 32 | 33 | public static isCurrentModel(input: any): boolean { 34 | const requireKeys: string[] = ['ref', 'ref_type', 'master_branch', 'pusher_type', 'repository', 'sender']; 35 | return isExistProperties(input, requireKeys); 36 | } 37 | 38 | convertToHubEvent(): GitHubEventModel { 39 | const eventType: GitHubEventType = 'CreateEvent'; 40 | const payload: GitHubPayloadInput = { 41 | // title: `Created ${this.ref_type} '${this.ref}'.` + this.description ? ` ${this.description}` : '', 42 | ref: this.ref, 43 | ref_type: this.ref_type, 44 | } 45 | 46 | const data: GitHubEventModel = { 47 | type: eventType, 48 | public: true, // TODO where get 49 | actor: GitHubUserMapper.import(this.sender), 50 | repository: GitHubRepositoryMapper.import(this.repository, 'event'), 51 | payload: GitHubPayloadMapper.import(eventType, payload), 52 | createdOn: firestore.Timestamp.now(), 53 | }; 54 | 55 | return data; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /web/src/app/monitors/monitors-list/monitors-list.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/scss/variable"; 2 | @import "../../../assets/scss/mixin.scss"; 3 | 4 | .monitor-list { 5 | @include pageContainer; 6 | 7 | &__header { 8 | @include pageHeader; 9 | 10 | &__add-button { 11 | color: #fff; 12 | background-color: $orange-3-color; 13 | } 14 | 15 | &__pings { 16 | border-radius: 4px; 17 | background-color: #fff; 18 | cursor: default; 19 | height: 35px; 20 | display: flex; 21 | font-size: 14px; 22 | align-items: center; 23 | justify-content: center; 24 | padding: 0 10px; 25 | font-weight: lighter; 26 | } 27 | 28 | &__content { 29 | 30 | &__project-title { 31 | @include ellipsisText; 32 | width: 20vw; 33 | } 34 | } 35 | } 36 | 37 | &__alert { 38 | @include cardContainer; 39 | 40 | &__card { 41 | font-size: 18px; 42 | font-weight: lighter; 43 | color: $dark-grey-3-color; 44 | } 45 | } 46 | 47 | &__card { 48 | @include cardContainer; 49 | 50 | &__header { 51 | @include header-icon; 52 | background-color: $orange-3-color; 53 | box-shadow: 0px 3px 6px $orange-3-shadow; 54 | } 55 | 56 | &__title { 57 | @include title; 58 | width: 55vw; 59 | 60 | &__subtitle { 61 | @include subtitle; 62 | width: 55vw; 63 | } 64 | } 65 | 66 | &__content { 67 | padding: 0; 68 | display: block; 69 | overflow: hidden; 70 | @include tableContainer; 71 | 72 | &__table { 73 | width: 100%; 74 | 75 | &__name { 76 | word-break: break-all; 77 | max-width: 30vw; 78 | } 79 | 80 | &__avatar { 81 | height: 35px; 82 | border-radius: 50%; 83 | object-fit: cover; 84 | } 85 | 86 | &__ping { 87 | @include ellipsisText; 88 | max-width: 15vw; 89 | } 90 | 91 | &__action { 92 | cursor: pointer; 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | 3 | match /databases/{database}/documents { 4 | 5 | function isSignedIn() { 6 | return request.auth.uid != null; 7 | } 8 | 9 | match /users/{user} { 10 | allow create: if isSignedIn(); 11 | allow get: if isSignedIn(); 12 | allow list: if isSignedIn() && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin; 13 | allow update, delete: if isSignedIn() && request.auth.uid == resource.data.uid; 14 | 15 | match /logins/{login} { 16 | allow read, write: if isSignedIn() && get(/databases/$(database)/documents/users/$(user)).data.uid == request.auth.uid; 17 | } 18 | } 19 | 20 | match /userStats/{user} { 21 | allow create, update: if isSignedIn(); 22 | allow get, list: if true; 23 | } 24 | 25 | match /projects/{project} { 26 | allow create: if isSignedIn(); 27 | allow get, list: if resource.data.type == 'public' || (isSignedIn() && (request.auth.uid in resource.data.access.admin || request.auth.uid in resource.data.access.readonly)); 28 | allow update, delete: if isSignedIn() && request.auth.uid in resource.data.access.admin; 29 | 30 | function projectDoc() { 31 | return get(/databases/$(database)/documents/projects/$(project)).data 32 | } 33 | 34 | match /pings/{ping} { 35 | allow create: if isSignedIn(); 36 | allow get, list: if resource.data.type == 'public' || request.auth.uid in projectDoc().access.admin || request.auth.uid in projectDoc().access.readonly; 37 | allow update, delete: if isSignedIn() && request.auth.uid in projectDoc().access.admin; 38 | } 39 | } 40 | 41 | match /repositories/{repository} { 42 | allow create: if false; 43 | allow get: if resource.data.private == false; 44 | allow list: if false; 45 | allow update: if false; 46 | allow delete: if false; 47 | } 48 | 49 | match /platform/stats { 50 | allow create: if false; 51 | allow get: if true; 52 | allow list: if false; 53 | allow update: if false; 54 | allow delete: if false; 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /web/src/app/shared/dialog/list/dialog-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 |
4 |
5 | folder_open 6 |
7 |
8 | {{ data.repositories.data.length || 0 }} | Your repositories 9 |
10 |
11 |
12 |
13 |
14 | Last updated 15 | {{ data.repositories.lastUpdated.toDate() | timeAgo }} 16 |
17 |
18 |
19 |

20 | 21 |
22 | 23 | 25 |
26 | 27 | lock 28 | lock_open 29 | 30 | 31 |
{{ repository.fullName }}
32 |
{{ repository.description }}
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 |
41 | 42 | 44 |
45 |
46 | -------------------------------------------------------------------------------- /web/src/app/main/components/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { Component, OnDestroy, OnInit } from '@angular/core'; 3 | 4 | // Breakpoints components 5 | import { Breakpoints, BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; 6 | 7 | // Rxjs operators 8 | import { Subscription } from 'rxjs'; 9 | 10 | // Dashboard hub model and services 11 | import { AuthenticationService } from '@core/services/index.service'; 12 | import { LoginAuditModel, ProfileModel } from '@shared/models/index.model'; 13 | 14 | /** 15 | * Profile component 16 | */ 17 | @Component({ 18 | selector: 'dashboard-profile', 19 | templateUrl: './profile.component.html', 20 | styleUrls: ['./profile.component.scss'], 21 | }) 22 | export class ProfileComponent implements OnInit, OnDestroy { 23 | 24 | private loginsSubscription: Subscription; 25 | public profile: ProfileModel; 26 | public logins: LoginAuditModel[] = []; 27 | public isSmallScreen: boolean; 28 | displayedColumns: string[] = ['title', 'description']; 29 | 30 | /** 31 | * Life cycle method 32 | * @param authService AuthenticationService 33 | * @param breakpointObserver BreakpointObserver 34 | */ 35 | constructor( 36 | public authService: AuthenticationService, 37 | private breakpointObserver: BreakpointObserver 38 | ) { 39 | } 40 | 41 | /** 42 | * Life cycle init method 43 | */ 44 | ngOnInit(): void { 45 | this.profile = this.authService.profile; 46 | this.loginsSubscription = this.authService 47 | .getLogins() 48 | .subscribe((logins: LoginAuditModel[]) => this.logins = logins); 49 | this.breakpointObserver 50 | .observe([Breakpoints.XSmall]) 51 | .subscribe((state: BreakpointState) => { 52 | if (state.matches) { 53 | this.displayedColumns = ['title']; 54 | this.isSmallScreen = true; 55 | } else { 56 | this.displayedColumns = ['title', 'description']; 57 | this.isSmallScreen = false; 58 | } 59 | }); 60 | } 61 | 62 | /** 63 | * Life cycle destroy method 64 | */ 65 | ngOnDestroy(): void { 66 | this.loginsSubscription 67 | .unsubscribe(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /web/src/app/projects/projects-routing.module.ts: -------------------------------------------------------------------------------- 1 | // Core modules 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | 5 | // Dashboard hub components 6 | import { EditProjectResolver } from '@core/resolvers/edit-project.resolver'; 7 | import { CreateEditProjectComponent } from './create-edit/create-edit.component'; 8 | import { ListMyProjectsComponent } from './list/list-my-projects.component'; 9 | 10 | // Application resolvers 11 | import { ViewProjectResolver } from '@core/resolvers/view-project.resolver'; 12 | import { ViewProjectComponent } from './view/view.component'; 13 | 14 | // Dashboard hub authentication guards 15 | import { AuthGuard } from '@core/guards/authentication.guard'; 16 | import { MyProjectsResolver } from '@core/resolvers/my-projects.resolver'; 17 | import { RepositoryResolver } from '@core/resolvers/repository.resolver'; 18 | import { RatingComponent } from './rating/rating.component'; 19 | 20 | const routes: Routes = [ 21 | { 22 | path: '', 23 | component: ListMyProjectsComponent, 24 | canActivate: [AuthGuard], 25 | resolve: { projects: MyProjectsResolver }, 26 | }, 27 | { 28 | path: 'create', 29 | component: CreateEditProjectComponent, 30 | canActivate: [AuthGuard], 31 | }, 32 | { 33 | path: ':projectUid', 34 | component: ViewProjectComponent, 35 | resolve: { project: ViewProjectResolver }, 36 | }, 37 | { 38 | path: ':projectUid/edit', 39 | component: CreateEditProjectComponent, 40 | resolve: { project: EditProjectResolver }, 41 | }, 42 | { 43 | path: ':projectUid/monitors', 44 | loadChildren: '../monitors/monitors.module#MonitorsModule', 45 | canActivate: [AuthGuard], 46 | }, 47 | { 48 | path: ':projectUid/tokens', 49 | loadChildren: '../tokens/tokens.module#TokensModule', 50 | }, 51 | { 52 | path: ':projectUid/rating/:repoUid', 53 | component: RatingComponent, 54 | resolve: { project: ViewProjectResolver, repository: RepositoryResolver }, 55 | }, 56 | ]; 57 | 58 | @NgModule({ 59 | imports: [RouterModule.forChild(routes)], 60 | exports: [RouterModule], 61 | }) 62 | export class ProjectsRoutingModule { } 63 | -------------------------------------------------------------------------------- /web/test/plugins/index.js: -------------------------------------------------------------------------------- 1 | const cucumber = require('cypress-cucumber-preprocessor').default; 2 | const admin = require('firebase-admin'); 3 | const serviceAccount = require('../../../firebase.enc.json'); 4 | 5 | const traverse = (o, fn) => { 6 | Object.entries(o).forEach((i) => { 7 | if (i[1] !== null && (typeof (i[1]) === 'object' || typeof (i[1]) === 'array')) { 8 | traverse(i[1], fn); 9 | } else { 10 | o[i[0]] = fn(i); 11 | } 12 | }); 13 | }; 14 | 15 | module.exports = (on, config) => { 16 | 17 | admin.initializeApp({ 18 | credential: admin.credential.cert(serviceAccount), 19 | databaseURL: `https://pipelinedashboard-${config.env.FIREBASE}.firebaseio.com` 20 | }); 21 | const db = admin.firestore(); 22 | 23 | let manipulate = (data) => { 24 | traverse(data, (item) => { 25 | switch (item[1]) { 26 | case 'DATETIME[NOW]': 27 | return admin.firestore.Timestamp.fromDate(new Date()) 28 | default: 29 | return item[1]; 30 | } 31 | }); 32 | 33 | return data; 34 | }; 35 | 36 | on('file:preprocessor', cucumber()); 37 | 38 | on('task', { 39 | 'db:update': (params) => db.collection(params.collection) 40 | .doc(params.id) 41 | .update({ 42 | [params.field]: params.value 43 | }), 44 | 45 | 'db:save': (params) => db.collection(params.collection) 46 | .doc(params.uid) 47 | .set({ 48 | ...manipulate(params.data), 49 | createdOn: admin.firestore.Timestamp.fromDate(new Date('2050-01-01')), 50 | updatedOn: admin.firestore.Timestamp.fromDate(new Date('2050-01-01')) 51 | }).catch((e) => console.log('DB:SAVE:ERROR: ', e)), 52 | 53 | 'db:delete:collection': (params) => db.collection(params.collection).get() 54 | .then((querySnapshot) => { 55 | const deletes = []; 56 | querySnapshot 57 | .forEach((doc) => { 58 | if ((doc.id).startsWith('test-')) { 59 | deletes.push(db.collection(params.collection).doc(doc.id).delete()); 60 | } 61 | }); 62 | 63 | return Promise.all(deletes).catch((e) => console.log('DB:SAVE:ERROR: ', e)); 64 | }), 65 | 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/issue-comment.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | 3 | // Dashboard mappers/models 4 | import { GitHubEventModel, GitHubEventType } from '../event.mapper'; 5 | import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; 6 | import { GitHubRepositoryMapper } from '../repository.mapper'; 7 | import { GitHubUserMapper } from '../user.mapper'; 8 | import { isExistProperties, HubEventActions, Issue, Repository, User } from './shared'; 9 | 10 | interface Comment { 11 | url: string; 12 | html_url: string; 13 | issue_url: string; 14 | id: number; 15 | node_id: string; 16 | user: User; 17 | created_at: string; 18 | updated_at: string; 19 | author_association: string; 20 | body: string; 21 | } 22 | 23 | type Action = 'created' | 'edited' | 'deleted'; 24 | 25 | export interface IssueCommentEventInput { 26 | action: Action; 27 | issue: Issue; 28 | comment: Comment; 29 | repository: Repository; 30 | sender: User; 31 | } 32 | 33 | export class IssueCommentEventModel implements IssueCommentEventInput, HubEventActions { 34 | action: Action; 35 | issue: Issue; 36 | comment: Comment; 37 | repository: Repository; 38 | sender: User; 39 | 40 | constructor(input: IssueCommentEventInput) { 41 | Object.assign(this, input); 42 | } 43 | 44 | public static isCurrentModel(input: any): boolean { 45 | const requireKeys: string[] = ['action', 'issue', 'comment', 'repository', 'sender']; 46 | return isExistProperties(input, requireKeys); 47 | } 48 | 49 | convertToHubEvent(): GitHubEventModel { 50 | const eventType: GitHubEventType = 'IssueCommentEvent'; 51 | const payload: GitHubPayloadInput = { 52 | action: this.action, 53 | comment: this.comment, 54 | }; 55 | 56 | const data: GitHubEventModel = { 57 | type: eventType, 58 | public: true, // TODO where get 59 | actor: GitHubUserMapper.import(this.sender), 60 | repository: GitHubRepositoryMapper.import(this.repository, 'event'), 61 | payload: GitHubPayloadMapper.import(eventType, payload), 62 | createdOn: firestore.Timestamp.now(), 63 | }; 64 | 65 | return data; 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /web/src/app/main/components/homepage/homepage.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../styles.scss"; 2 | @import "../../../../assets/scss/variable"; 3 | 4 | .home { 5 | @include pageContainer; 6 | 7 | &__title { 8 | @include pageHeader; 9 | 10 | } 11 | 12 | &__users-card { 13 | @include cardContainer; 14 | 15 | &__header { 16 | @include header-icon; 17 | background-color: $purple-color; 18 | box-shadow: 0px 3px 6px $purple-shadow; 19 | } 20 | 21 | &__title { 22 | @include title; 23 | 24 | &__subtitle { 25 | @include subtitle; 26 | } 27 | } 28 | 29 | &__content { 30 | padding: 0; 31 | display: block; 32 | overflow: hidden; 33 | @include tableContainer; 34 | 35 | &__table { 36 | width: 100%; 37 | padding: 0; 38 | 39 | &__avatar { 40 | height: 35px; 41 | border-radius: 50%; 42 | object-fit: cover; 43 | border: 1px solid $orange-color; 44 | margin-right: 20px; 45 | } 46 | 47 | &__icon { 48 | margin-right: 20px; 49 | height: 35px; 50 | } 51 | 52 | &__logo { 53 | width: 24px; 54 | } 55 | 56 | @media(max-width: $breakpoint-xs) { 57 | &__avatar { 58 | margin-right: 10px; 59 | } 60 | 61 | &__icon { 62 | margin-right: 10px; 63 | } 64 | } 65 | 66 | &__description { 67 | word-break: break-word; 68 | margin-right: 5px; 69 | } 70 | 71 | &__description-popular { 72 | &__text { 73 | @include ellipsisText; 74 | width: 35vw; 75 | } 76 | } 77 | 78 | &__title { 79 | min-width: 5vw; 80 | word-break: break-word; 81 | padding-right: 10px; 82 | 83 | @media (max-width: $breakpoint-sm) { 84 | min-width: 10vw; 85 | } 86 | } 87 | 88 | &__time { 89 | color: $dark-grey-color; 90 | font-size: 14px; 91 | font-weight: lighter; 92 | @include ellipsisText; 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /functions/src/mappers/github/webhook-event-response/status.ts: -------------------------------------------------------------------------------- 1 | import { isExistProperties, Repository, User } from './shared'; 2 | 3 | interface Author { 4 | name: string; 5 | email: string; 6 | date: string; 7 | } 8 | 9 | interface Verification { 10 | verified: boolean; 11 | reason: string; 12 | signature: string; 13 | payload: string; 14 | } 15 | 16 | interface Commit2 { 17 | author: Author; 18 | committer: Author; 19 | message: string; 20 | tree: { 21 | sha: string; 22 | url: string; 23 | }; 24 | url: string; 25 | comment_count: number; 26 | verification: Verification; 27 | } 28 | 29 | interface Commit { 30 | sha: string; 31 | node_id: string; 32 | commit: Commit2; 33 | url: string; 34 | html_url: string; 35 | comments_url: string; 36 | author: User; 37 | committer: User; 38 | parents: any[]; 39 | } 40 | 41 | 42 | interface Branch { 43 | name: string; 44 | commit: { 45 | sha: string; 46 | url: string; 47 | }; 48 | protected: boolean; 49 | } 50 | 51 | export interface StatusEventInput { 52 | id: number; 53 | sha: string; 54 | name: string; 55 | target_url: string; 56 | context: string; 57 | description: string; 58 | state: 'pending' | 'success' | 'failure' | 'error'; 59 | commit: Commit; 60 | branches: Branch[]; 61 | created_at: string; 62 | updated_at: string; 63 | repository: Repository; 64 | sender: User; 65 | } 66 | 67 | export class StatusEventModel implements StatusEventInput { 68 | id: number; 69 | sha: string; 70 | name: string; 71 | target_url: string; 72 | context: string; 73 | description: string; 74 | state: 'error' | 'pending' | 'success' | 'failure'; 75 | commit: Commit; 76 | branches: Branch[]; 77 | created_at: string; 78 | updated_at: string; 79 | repository: Repository; 80 | sender: User; 81 | 82 | constructor(input: StatusEventInput) { 83 | Object.assign(this, input); 84 | } 85 | 86 | public static isCurrentModel(input: any): boolean { 87 | const requireKeys: string[] = ['id', 'sha', 'name', 'target_url', 'context', 'description', 88 | 'state', 'commit', 'branches', 'created_at', 'updated_at', 'repository', 'sender']; 89 | return isExistProperties(input, requireKeys); 90 | } 91 | 92 | } 93 | --------------------------------------------------------------------------------