mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-22 17:56:30 -05:00
Fetch banner.txt from public URL instead of config (#1216)
This commit is contained in:
parent
e1d096ba1d
commit
0264e9a420
14 changed files with 161 additions and 87 deletions
1
cypress/fixtures/banner.txt
Normal file
1
cypress/fixtures/banner.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This is the mock banner call
|
3
cypress/fixtures/banner.txt.license
Normal file
3
cypress/fixtures/banner.txt.license
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
|
||||||
|
SPDX-License-Identifier: CC0-1.0
|
|
@ -4,31 +4,77 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { banner } from '../support/config'
|
const BANNER_LOCAL_STORAGE_KEY = 'banner.lastModified'
|
||||||
|
const MOCK_LAST_MODIFIED = 'mockETag'
|
||||||
|
const bannerMockContent = 'This is the mock banner call'
|
||||||
|
|
||||||
describe('Banner', () => {
|
describe('Banner', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
cy.intercept({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/mock-backend/public/banner.txt'
|
||||||
|
}, {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: { 'Last-Modified': MOCK_LAST_MODIFIED },
|
||||||
|
body: bannerMockContent
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.intercept({
|
||||||
|
method: 'HEAD',
|
||||||
|
url: '/mock-backend/public/banner.txt'
|
||||||
|
}, {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: { 'Last-Modified': MOCK_LAST_MODIFIED }
|
||||||
|
})
|
||||||
|
.as('headBanner')
|
||||||
|
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
expect(localStorage.getItem('bannerTimeStamp')).to.be.null
|
localStorage.removeItem(BANNER_LOCAL_STORAGE_KEY)
|
||||||
|
expect(localStorage.getItem(BANNER_LOCAL_STORAGE_KEY)).to.be.null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows the correct alert banner text', () => {
|
it('shows the correct alert banner text', () => {
|
||||||
cy.get('.alert-primary.show')
|
cy.get('[data-cy="motd-banner"]')
|
||||||
.contains(banner.text)
|
.contains(bannerMockContent)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can be dismissed', () => {
|
it('can be dismissed', () => {
|
||||||
cy.get('.alert-primary.show')
|
cy.get('[data-cy="motd-banner"]')
|
||||||
.contains(banner.text)
|
.contains(bannerMockContent)
|
||||||
cy.get('.alert-primary.show')
|
cy.get('button[data-cy="motd-dismiss"]')
|
||||||
.find('.fa-times')
|
|
||||||
.click()
|
.click()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(localStorage.getItem('bannerTimeStamp'))
|
expect(localStorage.getItem(BANNER_LOCAL_STORAGE_KEY))
|
||||||
.to
|
.to
|
||||||
.equal(banner.timestamp)
|
.equal(MOCK_LAST_MODIFIED)
|
||||||
})
|
})
|
||||||
cy.get('.alert-primary.show')
|
cy.get('[data-cy="no-motd-banner"]')
|
||||||
|
.should('exist')
|
||||||
|
cy.get('[data-cy="motd-banner"]')
|
||||||
|
.should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('won\'t show again on reload', () => {
|
||||||
|
cy.get('[data-cy="motd-banner"]')
|
||||||
|
.contains(bannerMockContent)
|
||||||
|
cy.get('button[data-cy="motd-dismiss"]')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
expect(localStorage.getItem(BANNER_LOCAL_STORAGE_KEY))
|
||||||
|
.to
|
||||||
|
.equal(MOCK_LAST_MODIFIED)
|
||||||
|
})
|
||||||
|
cy.get('[data-cy="no-motd-banner"]')
|
||||||
|
.should('exist')
|
||||||
|
cy.get('[data-cy="motd-banner"]')
|
||||||
|
.should('not.exist')
|
||||||
|
cy.reload()
|
||||||
|
cy.get('main')
|
||||||
|
.should('exist')
|
||||||
|
cy.wait('@headBanner')
|
||||||
|
cy.get('[data-cy="no-motd-banner"]')
|
||||||
|
.should('exist')
|
||||||
|
cy.get('[data-cy="motd-banner"]')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,11 +10,6 @@ declare namespace Cypress {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const banner = {
|
|
||||||
text: 'This is the mock banner call',
|
|
||||||
timestamp: '2020-05-22T20:46:08.962Z'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const branding = {
|
export const branding = {
|
||||||
name: 'DEMO Corp',
|
name: 'DEMO Corp',
|
||||||
logo: '/mock-backend/public/img/demo.png'
|
logo: '/mock-backend/public/img/demo.png'
|
||||||
|
@ -38,7 +33,6 @@ export const config = {
|
||||||
allowAnonymous: true,
|
allowAnonymous: true,
|
||||||
authProviders: authProviders,
|
authProviders: authProviders,
|
||||||
branding: branding,
|
branding: branding,
|
||||||
banner: banner,
|
|
||||||
customAuthNames: {
|
customAuthNames: {
|
||||||
ldap: 'FooBar',
|
ldap: 'FooBar',
|
||||||
oauth2: 'Olaf2',
|
oauth2: 'Olaf2',
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"content": "This is the test banner text",
|
|
||||||
"metadata": {
|
|
||||||
"id": "ABC11",
|
|
||||||
"alias": "banner",
|
|
||||||
"version": 2,
|
|
||||||
"viewCount": 0,
|
|
||||||
"updateTime": "2021-04-24T09:27:51.000Z",
|
|
||||||
"updateUser": {
|
|
||||||
"userName": "test",
|
|
||||||
"displayName": "Testy",
|
|
||||||
"photo": "",
|
|
||||||
"email": ""
|
|
||||||
},
|
|
||||||
"createTime": "2021-04-24T09:27:51.000Z",
|
|
||||||
"editedBy": []
|
|
||||||
}
|
|
||||||
}
|
|
1
public/mock-backend/public/banner.txt
Normal file
1
public/mock-backend/public/banner.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This is the test banner text
|
12
src/api/config/types.d.ts
vendored
12
src/api/config/types.d.ts
vendored
|
@ -9,7 +9,6 @@ export interface Config {
|
||||||
allowRegister: boolean,
|
allowRegister: boolean,
|
||||||
authProviders: AuthProvidersState,
|
authProviders: AuthProvidersState,
|
||||||
branding: BrandingConfig,
|
branding: BrandingConfig,
|
||||||
banner: BannerConfig,
|
|
||||||
customAuthNames: CustomAuthNames,
|
customAuthNames: CustomAuthNames,
|
||||||
useImageProxy: boolean,
|
useImageProxy: boolean,
|
||||||
specialUrls: SpecialUrls,
|
specialUrls: SpecialUrls,
|
||||||
|
@ -29,11 +28,6 @@ export interface BrandingConfig {
|
||||||
logo: string,
|
logo: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BannerConfig {
|
|
||||||
text: string
|
|
||||||
timestamp: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackendVersion {
|
export interface BackendVersion {
|
||||||
major: number
|
major: number
|
||||||
minor: number
|
minor: number
|
||||||
|
@ -63,7 +57,7 @@ export interface CustomAuthNames {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpecialUrls {
|
export interface SpecialUrls {
|
||||||
privacy: string,
|
privacy?: string,
|
||||||
termsOfUse: string,
|
termsOfUse?: string,
|
||||||
imprint: string,
|
imprint?: string,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { setBanner } from '../../../redux/banner/methods'
|
||||||
|
import { defaultFetchConfig } from '../../../api/utils'
|
||||||
|
|
||||||
|
export const BANNER_LOCAL_STORAGE_KEY = 'banner.lastModified'
|
||||||
|
|
||||||
|
export const fetchAndSetBanner = async (customizeAssetsUrl: string): Promise<void> => {
|
||||||
|
const cachedLastModified = window.localStorage.getItem(BANNER_LOCAL_STORAGE_KEY)
|
||||||
|
const bannerUrl = `${ customizeAssetsUrl }/banner.txt`
|
||||||
|
|
||||||
|
if (cachedLastModified) {
|
||||||
|
const response = await fetch(bannerUrl, {
|
||||||
|
...defaultFetchConfig,
|
||||||
|
method: 'HEAD'
|
||||||
|
})
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (response.headers.get('Last-Modified') === cachedLastModified) {
|
||||||
|
setBanner({
|
||||||
|
lastModified: cachedLastModified,
|
||||||
|
text: ''
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(bannerUrl, {
|
||||||
|
...defaultFetchConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const bannerText = await response.text()
|
||||||
|
|
||||||
|
const lastModified = response.headers.get('Last-Modified')
|
||||||
|
if (!lastModified) {
|
||||||
|
console.warn("'Last-Modified' not found for banner.txt!")
|
||||||
|
}
|
||||||
|
|
||||||
|
setBanner({
|
||||||
|
lastModified: lastModified,
|
||||||
|
text: bannerText
|
||||||
|
})
|
||||||
|
}
|
|
@ -5,7 +5,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getConfig } from '../../../api/config'
|
import { getConfig } from '../../../api/config'
|
||||||
import { setBanner } from '../../../redux/banner/methods'
|
|
||||||
import { setConfig } from '../../../redux/config/methods'
|
import { setConfig } from '../../../redux/config/methods'
|
||||||
|
|
||||||
export const fetchFrontendConfig = async (): Promise<void> => {
|
export const fetchFrontendConfig = async (): Promise<void> => {
|
||||||
|
@ -14,13 +13,4 @@ export const fetchFrontendConfig = async (): Promise<void> => {
|
||||||
return Promise.reject(new Error('Config empty!'))
|
return Promise.reject(new Error('Config empty!'))
|
||||||
}
|
}
|
||||||
setConfig(config)
|
setConfig(config)
|
||||||
|
|
||||||
const banner = config.banner
|
|
||||||
if (banner.text !== '') {
|
|
||||||
const lastAcknowledgedTimestamp = window.localStorage.getItem('bannerTimeStamp') || ''
|
|
||||||
setBanner({
|
|
||||||
...banner,
|
|
||||||
show: banner.text !== '' && banner.timestamp !== lastAcknowledgedTimestamp
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
import { setUpI18n } from './i18n'
|
import { setUpI18n } from './i18n'
|
||||||
import { refreshHistoryState } from '../../../redux/history/methods'
|
import { refreshHistoryState } from '../../../redux/history/methods'
|
||||||
|
import { fetchAndSetBanner } from './fetch-and-set-banner'
|
||||||
import { setApiUrl } from '../../../redux/api-url/methods'
|
import { setApiUrl } from '../../../redux/api-url/methods'
|
||||||
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
import { fetchAndSetUser } from '../../login-page/auth/utils'
|
||||||
import { fetchFrontendConfig } from './fetch-frontend-config'
|
import { fetchFrontendConfig } from './fetch-frontend-config'
|
||||||
|
@ -37,6 +38,9 @@ export const createSetUpTaskList = (frontendAssetsUrl: string, customizeAssetsUr
|
||||||
}, {
|
}, {
|
||||||
name: 'Fetch user information',
|
name: 'Fetch user information',
|
||||||
task: fetchAndSetUser()
|
task: fetchAndSetUser()
|
||||||
|
}, {
|
||||||
|
name: 'Banner',
|
||||||
|
task: fetchAndSetBanner(customizeAssetsUrl)
|
||||||
}, {
|
}, {
|
||||||
name: 'Load history state',
|
name: 'Load history state',
|
||||||
task: refreshHistoryState()
|
task: refreshHistoryState()
|
||||||
|
|
|
@ -1,41 +1,53 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import equal from 'fast-deep-equal'
|
import equal from 'fast-deep-equal'
|
||||||
import React from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Alert, Button } from 'react-bootstrap'
|
import { Alert, Button } from 'react-bootstrap'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { ApplicationState } from '../../../redux'
|
import { ApplicationState } from '../../../redux'
|
||||||
import { setBanner } from '../../../redux/banner/methods'
|
import { setBanner } from '../../../redux/banner/methods'
|
||||||
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
|
||||||
import { ShowIf } from '../show-if/show-if'
|
import { BANNER_LOCAL_STORAGE_KEY } from '../../application-loader/initializers/fetch-and-set-banner'
|
||||||
|
|
||||||
export const MotdBanner: React.FC = () => {
|
export const MotdBanner: React.FC = () => {
|
||||||
const bannerState = useSelector((state: ApplicationState) => state.banner, equal)
|
const bannerState = useSelector((state: ApplicationState) => state.banner, equal)
|
||||||
|
|
||||||
const dismissBanner = () => {
|
const dismissBanner = useCallback(() => {
|
||||||
setBanner({ ...bannerState, show: false })
|
if (bannerState.lastModified) {
|
||||||
window.localStorage.setItem('bannerTimeStamp', bannerState.timestamp)
|
window.localStorage.setItem(BANNER_LOCAL_STORAGE_KEY, bannerState.lastModified)
|
||||||
|
}
|
||||||
|
setBanner({
|
||||||
|
text: '',
|
||||||
|
lastModified: null
|
||||||
|
})
|
||||||
|
}, [bannerState])
|
||||||
|
|
||||||
|
if (bannerState.text === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bannerState.text) {
|
||||||
|
return <span data-cy={ 'no-motd-banner' }/>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={ bannerState.show }>
|
<Alert data-cy={ 'motd-banner' } variant="primary" dir="auto"
|
||||||
<Alert variant='primary' dir='auto' className='mb-0 text-center d-flex flex-row justify-content-center'>
|
className="mb-0 text-center d-flex flex-row justify-content-center">
|
||||||
<Link to='/s/banner' className='flex-grow-1 align-self-center text-black'>
|
<span className="flex-grow-1 align-self-center text-black">
|
||||||
{ bannerState.text }
|
{ bannerState.text }
|
||||||
</Link>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant='outline-primary'
|
data-cy={ 'motd-dismiss' }
|
||||||
size='sm'
|
variant="outline-primary"
|
||||||
className='mx-2'
|
size="sm"
|
||||||
onClick={ dismissBanner }>
|
className="mx-2"
|
||||||
<ForkAwesomeIcon icon='times'/>
|
onClick={ dismissBanner }>
|
||||||
</Button>
|
<ForkAwesomeIcon icon="times"/>
|
||||||
</Alert>
|
</Button>
|
||||||
</ShowIf>
|
</Alert>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,8 @@ import { Reducer } from 'redux'
|
||||||
import { BannerActions, BannerActionType, BannerState, SetBannerAction } from './types'
|
import { BannerActions, BannerActionType, BannerState, SetBannerAction } from './types'
|
||||||
|
|
||||||
export const initialState: BannerState = {
|
export const initialState: BannerState = {
|
||||||
show: false,
|
text: undefined,
|
||||||
text: '',
|
lastModified: null
|
||||||
timestamp: ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BannerReducer: Reducer<BannerState, BannerActions> = (state: BannerState = initialState, action: BannerActions) => {
|
export const BannerReducer: Reducer<BannerState, BannerActions> = (state: BannerState = initialState, action: BannerActions) => {
|
||||||
|
|
|
@ -15,11 +15,11 @@ export interface BannerActions extends Action<BannerActionType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetBannerAction extends BannerActions {
|
export interface SetBannerAction extends BannerActions {
|
||||||
|
type: BannerActionType.SET_BANNER
|
||||||
state: BannerState;
|
state: BannerState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BannerState {
|
export interface BannerState {
|
||||||
show: boolean
|
text: string | undefined
|
||||||
text: string
|
lastModified: string | null
|
||||||
timestamp: string
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,10 +28,6 @@ export const initialState: Config = {
|
||||||
name: '',
|
name: '',
|
||||||
logo: ''
|
logo: ''
|
||||||
},
|
},
|
||||||
banner: {
|
|
||||||
text: '',
|
|
||||||
timestamp: ''
|
|
||||||
},
|
|
||||||
customAuthNames: {
|
customAuthNames: {
|
||||||
ldap: '',
|
ldap: '',
|
||||||
oauth2: '',
|
oauth2: '',
|
||||||
|
|
Loading…
Reference in a new issue