Fetch banner.txt from public URL instead of config (#1216)

This commit is contained in:
Tilman Vatteroth 2021-05-03 21:57:55 +02:00 committed by GitHub
parent e1d096ba1d
commit 0264e9a420
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 161 additions and 87 deletions

View file

@ -0,0 +1 @@
This is the mock banner call

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: CC0-1.0

View file

@ -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')
}) })
}) })

View file

@ -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',

View file

@ -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": []
}
}

View file

@ -0,0 +1 @@
This is the test banner text

View file

@ -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,
} }

View file

@ -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
})
}

View file

@ -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
})
}
} }

View file

@ -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()

View file

@ -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>
) )
} }

View file

@ -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) => {

View file

@ -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
} }

View file

@ -28,10 +28,6 @@ export const initialState: Config = {
name: '', name: '',
logo: '' logo: ''
}, },
banner: {
text: '',
timestamp: ''
},
customAuthNames: { customAuthNames: {
ldap: '', ldap: '',
oauth2: '', oauth2: '',