Add about page

This commit is contained in:
Chocobozzz 2018-01-31 17:47:36 +01:00
parent 66b16cafb3
commit 36f9424ff1
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
20 changed files with 250 additions and 16 deletions

View File

@ -0,0 +1,23 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { MetaGuard } from '@ngx-meta/core'
import { AboutComponent } from './about.component'
const aboutRoutes: Routes = [
{
path: 'about',
component: AboutComponent,
canActivate: [ MetaGuard ],
data: {
meta: {
title: 'About'
}
}
}
]
@NgModule({
imports: [ RouterModule.forChild(aboutRoutes) ],
exports: [ RouterModule ]
})
export class AboutRoutingModule {}

View File

@ -0,0 +1,17 @@
<div class="margin-content">
<div class="title-page title-page-single">
Welcome to the {{ instanceName }} instance
</div>
<div class="description">
<div class="section-title">Description</div>
<div [innerHTML]="descriptionHTML"></div>
</div>
<div class="terms">
<div class="section-title">Terms</div>
<div [innerHTML]="termsHTML"></div>
</div>
</div>

View File

@ -0,0 +1,12 @@
@import '_variables';
@import '_mixins';
.section-title {
font-weight: $font-semibold;
font-size: 20px;
margin-bottom: 5px;
}
.description {
margin-bottom: 30px;
}

View File

@ -0,0 +1,38 @@
import { Component, OnInit } from '@angular/core'
import { ServerService } from '@app/core'
import { MarkdownService } from '@app/videos/shared'
import { NotificationsService } from 'angular2-notifications'
@Component({
selector: 'my-about',
templateUrl: './about.component.html',
styleUrls: [ './about.component.scss' ]
})
export class AboutComponent implements OnInit {
descriptionHTML = ''
termsHTML = ''
constructor (
private notificationsService: NotificationsService,
private serverService: ServerService,
private markdownService: MarkdownService
) {}
get instanceName () {
return this.serverService.getConfig().instance.name
}
ngOnInit () {
this.serverService.getAbout()
.subscribe(
res => {
this.descriptionHTML = this.markdownService.markdownToHTML(res.instance.description)
this.termsHTML = this.markdownService.markdownToHTML(res.instance.terms)
},
err => this.notificationsService.error('Error', err)
)
}
}

View File

@ -0,0 +1,24 @@
import { NgModule } from '@angular/core'
import { AboutRoutingModule } from './about-routing.module'
import { AboutComponent } from './about.component'
import { SharedModule } from '../shared'
@NgModule({
imports: [
AboutRoutingModule,
SharedModule
],
declarations: [
AboutComponent
],
exports: [
AboutComponent
],
providers: [
]
})
export class AboutModule { }

View File

@ -0,0 +1,3 @@
export * from './about-routing.module'
export * from './about.component'
export * from './about.module'

View File

@ -6,7 +6,7 @@
<a id="peertube-title" [routerLink]="['/videos/list']" title="Homepage"> <a id="peertube-title" [routerLink]="['/videos/list']" title="Homepage">
<span class="icon icon-logo"></span> <span class="icon icon-logo"></span>
PeerTube {{ instanceName }}
</a> </a>
</div> </div>

View File

@ -34,6 +34,10 @@ export class AppComponent implements OnInit {
return this.serverService.getConfig().serverVersion return this.serverService.getConfig().serverVersion
} }
get instanceName () {
return this.serverService.getConfig().instance.name
}
ngOnInit () { ngOnInit () {
this.authService.loadClientCredentials() this.authService.loadClientCredentials()

View File

@ -1,5 +1,6 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
import { AboutModule } from '@app/about'
import { ResetPasswordModule } from '@app/reset-password' import { ResetPasswordModule } from '@app/reset-password'
import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core' import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
@ -51,6 +52,7 @@ export function metaFactory (): MetaLoader {
SignupModule, SignupModule,
SharedModule, SharedModule,
VideosModule, VideosModule,
AboutModule,
MetaModule.forRoot({ MetaModule.forRoot({
provide: MetaLoader, provide: MetaLoader,

View File

@ -3,12 +3,14 @@ import { Injectable } from '@angular/core'
import 'rxjs/add/operator/do' import 'rxjs/add/operator/do'
import { ReplaySubject } from 'rxjs/ReplaySubject' import { ReplaySubject } from 'rxjs/ReplaySubject'
import { ServerConfig } from '../../../../../shared' import { ServerConfig } from '../../../../../shared'
import { About } from '../../../../../shared/models/config/about.model'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
@Injectable() @Injectable()
export class ServerService { export class ServerService {
private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/' private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/'
private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
videoPrivaciesLoaded = new ReplaySubject<boolean>(1) videoPrivaciesLoaded = new ReplaySubject<boolean>(1)
videoCategoriesLoaded = new ReplaySubject<boolean>(1) videoCategoriesLoaded = new ReplaySubject<boolean>(1)
@ -16,6 +18,9 @@ export class ServerService {
videoLanguagesLoaded = new ReplaySubject<boolean>(1) videoLanguagesLoaded = new ReplaySubject<boolean>(1)
private config: ServerConfig = { private config: ServerConfig = {
instance: {
name: 'PeerTube'
},
serverVersion: 'Unknown', serverVersion: 'Unknown',
signup: { signup: {
allowed: false allowed: false
@ -40,11 +45,14 @@ export class ServerService {
private videoLanguages: Array<{ id: number, label: string }> = [] private videoLanguages: Array<{ id: number, label: string }> = []
private videoPrivacies: Array<{ id: number, label: string }> = [] private videoPrivacies: Array<{ id: number, label: string }> = []
constructor (private http: HttpClient) {} constructor (private http: HttpClient) {
this.loadConfigLocally()
}
loadConfig () { loadConfig () {
this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL) this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL)
.subscribe(data => this.config = data) .do(this.saveConfigLocally)
.subscribe(data => this.config = data)
} }
loadVideoCategories () { loadVideoCategories () {
@ -83,6 +91,10 @@ export class ServerService {
return this.videoPrivacies return this.videoPrivacies
} }
getAbout () {
return this.http.get<About>(ServerService.BASE_CONFIG_URL + '/about')
}
private loadVideoAttributeEnum ( private loadVideoAttributeEnum (
attributeName: 'categories' | 'licences' | 'languages' | 'privacies', attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
hashToPopulate: { id: number, label: string }[], hashToPopulate: { id: number, label: string }[],
@ -101,4 +113,21 @@ export class ServerService {
notifier.next(true) notifier.next(true)
}) })
} }
private saveConfigLocally (config: ServerConfig) {
localStorage.setItem(ServerService.CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config))
}
private loadConfigLocally () {
const configString = localStorage.getItem(ServerService.CONFIG_LOCAL_STORAGE_KEY)
if (configString) {
try {
const parsed = JSON.parse(configString)
Object.assign(this.config, parsed)
} catch (err) {
console.error('Cannot parse config saved in local storage.', err)
}
}
}
} }

View File

@ -45,12 +45,17 @@
</a> </a>
</div> </div>
<div *ngIf="userHasAdminAccess" class="panel-block"> <div class="panel-block">
<div class="block-title">More</div> <div class="block-title">More</div>
<a [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active"> <a *ngIf="userHasAdminAccess" [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
<span class="icon icon-administration"></span> <span class="icon icon-administration"></span>
Administration Administration
</a> </a>
<a routerLink="/about" routerLinkActive="active">
<span class="icon icon-about"></span>
About
</a>
</div> </div>
</menu> </menu>

View File

@ -132,6 +132,13 @@ menu {
background-image: url('../../assets/images/menu/administration.svg'); background-image: url('../../assets/images/menu/administration.svg');
} }
&.icon-about {
width: 23px;
height: 23px;
background-image: url('../../assets/images/menu/about.svg');
}
} }
} }
} }

View File

@ -3,7 +3,7 @@ import { Validators } from '@angular/forms'
export const INSTANCE_NAME = { export const INSTANCE_NAME = {
VALIDATORS: [ Validators.required ], VALIDATORS: [ Validators.required ],
MESSAGES: { MESSAGES: {
'required': 'Instance name is required.', 'required': 'Instance name is required.'
} }
} }

View File

@ -7,12 +7,13 @@ export class MarkdownService {
private markdownIt: MarkdownIt.MarkdownIt private markdownIt: MarkdownIt.MarkdownIt
constructor () { constructor () {
this.markdownIt = new MarkdownIt('zero', { linkify: true }) this.markdownIt = new MarkdownIt('zero', { linkify: true, breaks: true })
.enable('linkify') .enable('linkify')
.enable('autolink') .enable('autolink')
.enable('emphasis') .enable('emphasis')
.enable('link') .enable('link')
.enable('newline') .enable('newline')
.enable('list')
this.setTargetToLinks() this.setTargetToLinks()
} }

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard-4" transform="translate(-400.000000, -247.000000)">
<g id="69" transform="translate(400.000000, 247.000000)">
<circle id="Oval-7" stroke="#808080" stroke-width="2" cx="12" cy="12" r="10"></circle>
<path d="M12.016,14.544 C12.384,14.544 12.64,14.256 12.704,13.904 L12.768,13.168 C14.544,12.864 16,11.952 16,9.936 L16,9.904 C16,7.904 14.48,6.656 12.24,6.656 C10.768,6.656 9.696,7.184 8.848,7.984 C8.624,8.176 8.528,8.432 8.528,8.672 C8.528,9.152 8.928,9.552 9.424,9.552 C9.648,9.552 9.856,9.456 10.016,9.328 C10.656,8.752 11.344,8.448 12.192,8.448 C13.344,8.448 14.032,9.072 14.032,9.968 L14.032,10 C14.032,11.008 13.2,11.584 11.696,11.728 C11.264,11.776 11.008,12.096 11.072,12.528 L11.232,13.904 C11.28,14.272 11.552,14.544 11.92,14.544 L12.016,14.544 Z M10.784,16.816 L10.784,16.976 C10.784,17.6 11.264,18.08 11.92,18.08 C12.576,18.08 13.056,17.6 13.056,16.976 L13.056,16.816 C13.056,16.192 12.576,15.712 11.92,15.712 C11.264,15.712 10.784,16.192 10.784,16.816 Z" id="?" fill="#808080"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,19 +1,22 @@
import * as express from 'express' import * as express from 'express'
import { omit } from 'lodash'
import { ServerConfig, UserRight } from '../../../shared' import { ServerConfig, UserRight } from '../../../shared'
import { About } from '../../../shared/models/config/about.model'
import { CustomConfig } from '../../../shared/models/config/custom-config.model' import { CustomConfig } from '../../../shared/models/config/custom-config.model'
import { unlinkPromise, writeFilePromise } from '../../helpers/core-utils' import { unlinkPromise, writeFilePromise } from '../../helpers/core-utils'
import { isSignupAllowed } from '../../helpers/utils' import { isSignupAllowed } from '../../helpers/utils'
import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers' import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers'
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
import { customConfigUpdateValidator } from '../../middlewares/validators/config' import { customConfigUpdateValidator } from '../../middlewares/validators/config'
import { omit } from 'lodash'
const packageJSON = require('../../../../package.json') const packageJSON = require('../../../../package.json')
const configRouter = express.Router() const configRouter = express.Router()
configRouter.get('/about', getAbout)
configRouter.get('/', configRouter.get('/',
asyncMiddleware(getConfig) asyncMiddleware(getConfig)
) )
configRouter.get('/custom', configRouter.get('/custom',
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
@ -39,6 +42,9 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
.map(r => parseInt(r, 10)) .map(r => parseInt(r, 10))
const json: ServerConfig = { const json: ServerConfig = {
instance: {
name: CONFIG.INSTANCE.NAME
},
serverVersion: packageJSON.version, serverVersion: packageJSON.version,
signup: { signup: {
allowed allowed
@ -64,6 +70,18 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
return res.json(json) return res.json(json)
} }
function getAbout (req: express.Request, res: express.Response, next: express.NextFunction) {
const about: About = {
instance: {
name: CONFIG.INSTANCE.NAME,
description: CONFIG.INSTANCE.DESCRIPTION,
terms: CONFIG.INSTANCE.TERMS
}
}
return res.json(about).end()
}
async function getCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { async function getCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) {
const data = customConfig() const data = customConfig()

View File

@ -2,7 +2,8 @@
import 'mocha' import 'mocha'
import * as chai from 'chai' import * as chai from 'chai'
import { deleteCustomConfig, killallServers, reRunServer } from '../../utils' import { About } from '../../../../shared/models/config/about.model'
import { deleteCustomConfig, getAbout, killallServers, reRunServer } from '../../utils'
const expect = chai.expect const expect = chai.expect
import { import {
@ -108,6 +109,7 @@ describe('Test config', function () {
expect(data.instance.name).to.equal('PeerTube updated') expect(data.instance.name).to.equal('PeerTube updated')
expect(data.instance.description).to.equal('my super description') expect(data.instance.description).to.equal('my super description')
expect(data.instance.terms).to.equal('my super terms') expect(data.instance.terms).to.equal('my super terms')
expect(data.cache.previews.size).to.equal(2)
expect(data.signup.enabled).to.be.false expect(data.signup.enabled).to.be.false
expect(data.signup.limit).to.equal(5) expect(data.signup.limit).to.equal(5)
expect(data.admin.email).to.equal('superadmin1@example.com') expect(data.admin.email).to.equal('superadmin1@example.com')
@ -131,6 +133,9 @@ describe('Test config', function () {
const res = await getCustomConfig(server.url, server.accessToken) const res = await getCustomConfig(server.url, server.accessToken)
const data = res.body const data = res.body
expect(data.instance.name).to.equal('PeerTube updated')
expect(data.instance.description).to.equal('my super description')
expect(data.instance.terms).to.equal('my super terms')
expect(data.cache.previews.size).to.equal(2) expect(data.cache.previews.size).to.equal(2)
expect(data.signup.enabled).to.be.false expect(data.signup.enabled).to.be.false
expect(data.signup.limit).to.equal(5) expect(data.signup.limit).to.equal(5)
@ -145,6 +150,15 @@ describe('Test config', function () {
expect(data.transcoding.resolutions['1080p']).to.be.false expect(data.transcoding.resolutions['1080p']).to.be.false
}) })
it('Should fetch the about information', async function () {
const res = await getAbout(server.url)
const data: About = res.body
expect(data.instance.name).to.equal('PeerTube updated')
expect(data.instance.description).to.equal('my super description')
expect(data.instance.terms).to.equal('my super terms')
})
it('Should remove the custom configuration', async function () { it('Should remove the custom configuration', async function () {
this.timeout(10000) this.timeout(10000)

View File

@ -1,15 +1,24 @@
import * as request from 'supertest'
import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../' import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../'
import { CustomConfig } from '../../../../shared/models/config/custom-config.model' import { CustomConfig } from '../../../../shared/models/config/custom-config.model'
function getConfig (url: string) { function getConfig (url: string) {
const path = '/api/v1/config' const path = '/api/v1/config'
return request(url) return makeGetRequest({
.get(path) url,
.set('Accept', 'application/json') path,
.expect(200) statusCodeExpected: 200
.expect('Content-Type', /json/) })
}
function getAbout (url: string) {
const path = '/api/v1/config/about'
return makeGetRequest({
url,
path,
statusCodeExpected: 200
})
} }
function getCustomConfig (url: string, token: string, statusCodeExpected = 200) { function getCustomConfig (url: string, token: string, statusCodeExpected = 200) {
@ -52,5 +61,6 @@ export {
getConfig, getConfig,
getCustomConfig, getCustomConfig,
updateCustomConfig, updateCustomConfig,
getAbout,
deleteCustomConfig deleteCustomConfig
} }

View File

@ -0,0 +1,7 @@
export interface About {
instance: {
name: string
description: string
terms: string
}
}

View File

@ -1,11 +1,18 @@
export interface ServerConfig { export interface ServerConfig {
serverVersion: string, serverVersion: string
instance: {
name: string
}
signup: { signup: {
allowed: boolean allowed: boolean
} }
transcoding: { transcoding: {
enabledResolutions: number[] enabledResolutions: number[]
} }
avatar: { avatar: {
file: { file: {
size: { size: {
@ -14,6 +21,7 @@ export interface ServerConfig {
extensions: string[] extensions: string[]
} }
} }
video: { video: {
file: { file: {
extensions: string[] extensions: string[]