From 0264e9a420f7ba29d84dd16687c0da417406a653 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Mon, 3 May 2021 21:57:55 +0200 Subject: [PATCH] Fetch banner.txt from public URL instead of config (#1216) --- cypress/fixtures/banner.txt | 1 + cypress/fixtures/banner.txt.license | 3 + cypress/integration/banner.spec.ts | 68 ++++++++++++++++--- cypress/support/config.ts | 6 -- .../mock-backend/api/private/notes/banner-get | 18 ----- public/mock-backend/public/banner.txt | 1 + src/api/config/types.d.ts | 12 +--- .../initializers/fetch-and-set-banner.ts | 52 ++++++++++++++ .../initializers/fetch-frontend-config.ts | 10 --- .../application-loader/initializers/index.ts | 4 ++ .../common/motd-banner/motd-banner.tsx | 58 +++++++++------- src/redux/banner/reducers.ts | 5 +- src/redux/banner/types.ts | 6 +- src/redux/config/reducers.ts | 4 -- 14 files changed, 161 insertions(+), 87 deletions(-) create mode 100644 cypress/fixtures/banner.txt create mode 100644 cypress/fixtures/banner.txt.license delete mode 100644 public/mock-backend/api/private/notes/banner-get create mode 100644 public/mock-backend/public/banner.txt create mode 100644 src/components/application-loader/initializers/fetch-and-set-banner.ts diff --git a/cypress/fixtures/banner.txt b/cypress/fixtures/banner.txt new file mode 100644 index 000000000..bb11f3e01 --- /dev/null +++ b/cypress/fixtures/banner.txt @@ -0,0 +1 @@ +This is the mock banner call diff --git a/cypress/fixtures/banner.txt.license b/cypress/fixtures/banner.txt.license new file mode 100644 index 000000000..078e5a9ac --- /dev/null +++ b/cypress/fixtures/banner.txt.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + +SPDX-License-Identifier: CC0-1.0 diff --git a/cypress/integration/banner.spec.ts b/cypress/integration/banner.spec.ts index 6dcbcd34f..dde571d5c 100644 --- a/cypress/integration/banner.spec.ts +++ b/cypress/integration/banner.spec.ts @@ -4,31 +4,77 @@ * 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', () => { 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('/') - 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', () => { - cy.get('.alert-primary.show') - .contains(banner.text) + cy.get('[data-cy="motd-banner"]') + .contains(bannerMockContent) }) it('can be dismissed', () => { - cy.get('.alert-primary.show') - .contains(banner.text) - cy.get('.alert-primary.show') - .find('.fa-times') + cy.get('[data-cy="motd-banner"]') + .contains(bannerMockContent) + cy.get('button[data-cy="motd-dismiss"]') .click() .then(() => { - expect(localStorage.getItem('bannerTimeStamp')) + expect(localStorage.getItem(BANNER_LOCAL_STORAGE_KEY)) .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') }) }) diff --git a/cypress/support/config.ts b/cypress/support/config.ts index 68037d069..c675b8ab1 100644 --- a/cypress/support/config.ts +++ b/cypress/support/config.ts @@ -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 = { name: 'DEMO Corp', logo: '/mock-backend/public/img/demo.png' @@ -38,7 +33,6 @@ export const config = { allowAnonymous: true, authProviders: authProviders, branding: branding, - banner: banner, customAuthNames: { ldap: 'FooBar', oauth2: 'Olaf2', diff --git a/public/mock-backend/api/private/notes/banner-get b/public/mock-backend/api/private/notes/banner-get deleted file mode 100644 index 566b40220..000000000 --- a/public/mock-backend/api/private/notes/banner-get +++ /dev/null @@ -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": [] - } -} diff --git a/public/mock-backend/public/banner.txt b/public/mock-backend/public/banner.txt new file mode 100644 index 000000000..0e0d4dacd --- /dev/null +++ b/public/mock-backend/public/banner.txt @@ -0,0 +1 @@ +This is the test banner text diff --git a/src/api/config/types.d.ts b/src/api/config/types.d.ts index 71ddf3f5b..bac4afc4f 100644 --- a/src/api/config/types.d.ts +++ b/src/api/config/types.d.ts @@ -9,7 +9,6 @@ export interface Config { allowRegister: boolean, authProviders: AuthProvidersState, branding: BrandingConfig, - banner: BannerConfig, customAuthNames: CustomAuthNames, useImageProxy: boolean, specialUrls: SpecialUrls, @@ -29,11 +28,6 @@ export interface BrandingConfig { logo: string, } -export interface BannerConfig { - text: string - timestamp: string -} - export interface BackendVersion { major: number minor: number @@ -63,7 +57,7 @@ export interface CustomAuthNames { } export interface SpecialUrls { - privacy: string, - termsOfUse: string, - imprint: string, + privacy?: string, + termsOfUse?: string, + imprint?: string, } diff --git a/src/components/application-loader/initializers/fetch-and-set-banner.ts b/src/components/application-loader/initializers/fetch-and-set-banner.ts new file mode 100644 index 000000000..462e6deb4 --- /dev/null +++ b/src/components/application-loader/initializers/fetch-and-set-banner.ts @@ -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 => { + 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 + }) +} diff --git a/src/components/application-loader/initializers/fetch-frontend-config.ts b/src/components/application-loader/initializers/fetch-frontend-config.ts index b3e82c305..5030cab39 100644 --- a/src/components/application-loader/initializers/fetch-frontend-config.ts +++ b/src/components/application-loader/initializers/fetch-frontend-config.ts @@ -5,7 +5,6 @@ */ import { getConfig } from '../../../api/config' -import { setBanner } from '../../../redux/banner/methods' import { setConfig } from '../../../redux/config/methods' export const fetchFrontendConfig = async (): Promise => { @@ -14,13 +13,4 @@ export const fetchFrontendConfig = async (): Promise => { return Promise.reject(new Error('Config empty!')) } setConfig(config) - - const banner = config.banner - if (banner.text !== '') { - const lastAcknowledgedTimestamp = window.localStorage.getItem('bannerTimeStamp') || '' - setBanner({ - ...banner, - show: banner.text !== '' && banner.timestamp !== lastAcknowledgedTimestamp - }) - } } diff --git a/src/components/application-loader/initializers/index.ts b/src/components/application-loader/initializers/index.ts index b87ab4d3a..c72253f20 100644 --- a/src/components/application-loader/initializers/index.ts +++ b/src/components/application-loader/initializers/index.ts @@ -6,6 +6,7 @@ import { setUpI18n } from './i18n' import { refreshHistoryState } from '../../../redux/history/methods' +import { fetchAndSetBanner } from './fetch-and-set-banner' import { setApiUrl } from '../../../redux/api-url/methods' import { fetchAndSetUser } from '../../login-page/auth/utils' import { fetchFrontendConfig } from './fetch-frontend-config' @@ -37,6 +38,9 @@ export const createSetUpTaskList = (frontendAssetsUrl: string, customizeAssetsUr }, { name: 'Fetch user information', task: fetchAndSetUser() + }, { + name: 'Banner', + task: fetchAndSetBanner(customizeAssetsUrl) }, { name: 'Load history state', task: refreshHistoryState() diff --git a/src/components/common/motd-banner/motd-banner.tsx b/src/components/common/motd-banner/motd-banner.tsx index 50c808631..88dc66be0 100644 --- a/src/components/common/motd-banner/motd-banner.tsx +++ b/src/components/common/motd-banner/motd-banner.tsx @@ -1,41 +1,53 @@ /* - SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - - SPDX-License-Identifier: AGPL-3.0-only + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only */ import equal from 'fast-deep-equal' -import React from 'react' +import React, { useCallback } from 'react' import { Alert, Button } from 'react-bootstrap' import { useSelector } from 'react-redux' -import { Link } from 'react-router-dom' import { ApplicationState } from '../../../redux' import { setBanner } from '../../../redux/banner/methods' 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 = () => { const bannerState = useSelector((state: ApplicationState) => state.banner, equal) - const dismissBanner = () => { - setBanner({ ...bannerState, show: false }) - window.localStorage.setItem('bannerTimeStamp', bannerState.timestamp) + const dismissBanner = useCallback(() => { + if (bannerState.lastModified) { + window.localStorage.setItem(BANNER_LOCAL_STORAGE_KEY, bannerState.lastModified) + } + setBanner({ + text: '', + lastModified: null + }) + }, [bannerState]) + + if (bannerState.text === undefined) { + return null + } + + if (!bannerState.text) { + return } return ( - - - - { bannerState.text } - - - - + + + { bannerState.text } + + + ) } diff --git a/src/redux/banner/reducers.ts b/src/redux/banner/reducers.ts index b305c5734..d81fca1a7 100644 --- a/src/redux/banner/reducers.ts +++ b/src/redux/banner/reducers.ts @@ -8,9 +8,8 @@ import { Reducer } from 'redux' import { BannerActions, BannerActionType, BannerState, SetBannerAction } from './types' export const initialState: BannerState = { - show: false, - text: '', - timestamp: '' + text: undefined, + lastModified: null } export const BannerReducer: Reducer = (state: BannerState = initialState, action: BannerActions) => { diff --git a/src/redux/banner/types.ts b/src/redux/banner/types.ts index 52f0e53dd..c2bdaefd6 100644 --- a/src/redux/banner/types.ts +++ b/src/redux/banner/types.ts @@ -15,11 +15,11 @@ export interface BannerActions extends Action { } export interface SetBannerAction extends BannerActions { + type: BannerActionType.SET_BANNER state: BannerState; } export interface BannerState { - show: boolean - text: string - timestamp: string + text: string | undefined + lastModified: string | null } diff --git a/src/redux/config/reducers.ts b/src/redux/config/reducers.ts index 11d3dcb13..cbe520e03 100644 --- a/src/redux/config/reducers.ts +++ b/src/redux/config/reducers.ts @@ -28,10 +28,6 @@ export const initialState: Config = { name: '', logo: '' }, - banner: { - text: '', - timestamp: '' - }, customAuthNames: { ldap: '', oauth2: '',