Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(PPDSC-2625): fail build on cms issues #564

Merged
merged 4 commits into from
Jan 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 49 additions & 47 deletions site/pages/about/roadmap.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,51 @@
import React from 'react';
import {
getSSRId,
InlineMessage,
LinkInline,
TextBlock,
UnorderedList,
getSSRId,
InlineMessage,
} from 'newskit';
import ReactMarkdown, {ReactMarkdownOptions} from 'react-markdown';
import {getSheets} from '../../utils/google-sheet';
import {formatSheetData, getCMSList} from '../../utils/google-sheet/utils';
import {
getSheets,
PageCMSPrefixedProps,
PageCMSRequiredProps,
} from '../../utils/google-sheet';
import {
getCMSPropsWithPrefix,
parseCMSResponse,
} from '../../utils/google-sheet/utils';
import {AboutPageTemplate} from '../../templates/about-page-template';
import {LayoutProps} from '../../components/layout';
import {
ContentSection,
ContentPrimary,
ContentSection,
} from '../../components/content-structure';
import {ComponentPageCell} from '../../components/layout-cells';

interface RoadmapContent {
intro_name: string;
intro_secondary: string;
intro_description: string;
intro_hero_illustration: string;
intro_date: string;
current_headline: string;
current_description: string;
comingup_headline: string;
comingup_description: string;
future_headline: string;
future_description: string;
[current_li_keys: `current_li${string}`]: string;
[comingup_li_keys: `comingup_li${string}`]: string;
[future_li_keys: `future_li${string}`]: string;
enum RequiredKeys {
intro_name = 'intro_name',
intro_secondary = 'intro_secondary',
intro_description = 'intro_description',
intro_hero_illustration = 'intro_hero_illustration',
intro_date = 'intro_date',
current_headline = 'current_headline',
current_description = 'current_description',
comingup_headline = 'comingup_headline',
comingup_description = 'comingup_description',
future_headline = 'future_headline',
future_description = 'future_description',
}

const roadmapFallbackContent: RoadmapContent = {
intro_name: 'Roadmap - fallback',
intro_description:
'Fallback - NewsKit’s Design System team is busy building and planning to help you build better products faster.',
intro_hero_illustration: 'components/hero-roadmap-illustration',
intro_secondary:
'The roadmap is a living document, and it is likely that priorities will change. See our Trello board for more details on the roadmap.',
intro_date: 'Last Updated: 6 December 2022',
current_headline: 'Current Quarter',
current_description: 'What we are working on:',
comingup_headline: 'Coming Up',
comingup_description: 'The focus for the next quarter:',
future_headline: 'Future',
future_description: 'Ideas we plan to look at:',
};
enum DynamicKeyPrefixes {
current_li_ = 'current_li_',
comingup_li_ = 'comingup_li_',
future_li_ = 'future_li_',
}

type RoadmapContent = PageCMSRequiredProps<RequiredKeys> &
PageCMSPrefixedProps<DynamicKeyPrefixes>;

const FormatMarkdown: React.FC<ReactMarkdownOptions> = ({children}) => (
/* eslint-disable @typescript-eslint/no-shadow */
Expand Down Expand Up @@ -85,16 +82,20 @@ const Roadmap = ({

const introSecondary = content.intro_secondary;

const currentList = getCMSList(content, 'current_li').map(entry => (
<FormatMarkdown key={entry[0]}>{entry[1]}</FormatMarkdown>
));
const currentList = getCMSPropsWithPrefix<typeof DynamicKeyPrefixes>(
content,
'current_li_',
).map(([k, v]) => <FormatMarkdown key={k}>{v}</FormatMarkdown>);

const comingupList = getCMSPropsWithPrefix<typeof DynamicKeyPrefixes>(
content,
'comingup_li_',
).map(([k, v]) => <FormatMarkdown key={k}>{v}</FormatMarkdown>);

const comingupList = getCMSList(content, 'comingup_li').map(entry => (
<FormatMarkdown key={entry[0]}>{entry[1]}</FormatMarkdown>
));
const futureList = getCMSList(content, 'future_li').map(entry => (
<FormatMarkdown key={entry[0]}>{entry[1]}</FormatMarkdown>
));
const futureList = getCMSPropsWithPrefix<typeof DynamicKeyPrefixes>(
content,
'future_li_',
).map(([k, v]) => <FormatMarkdown key={k}>{v}</FormatMarkdown>);

if (currentList.length && comingupList.length && futureList.length) {
return (
Expand Down Expand Up @@ -256,10 +257,11 @@ const Roadmap = ({
};
export default Roadmap;

// This function is called at build time and the response is passed to the page
// component as props.
export async function getStaticProps() {
const cmsData = await getSheets('Roadmap');
const content = {...roadmapFallbackContent, ...formatSheetData(cmsData)};
const content = parseCMSResponse(cmsData, {
required: RequiredKeys,
dynamic: DynamicKeyPrefixes,
});
return {props: {content}};
}
25 changes: 10 additions & 15 deletions site/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import Layout, {LayoutProps} from '../components/layout';
import {IconFilledLaunch} from '../../src/icons';
import {GridLayoutProps} from '../../src/grid-layout/types';
import {fetchGitHubReleases} from '../utils/release-notes/functions';
import {getSheets} from '../utils/google-sheet';
import {formatSheetData} from '../utils/google-sheet/utils';
import {getSheets, PageCMSRequiredProps} from '../utils/google-sheet';
import {parseCMSResponse} from '../utils/google-sheet/utils';

const GRID_SECTION_OVERRIDES: GridLayoutProps['overrides'] = {
maxWidth: '1150px',
Expand All @@ -28,19 +28,14 @@ const GRID_SECTION_OVERRIDES: GridLayoutProps['overrides'] = {
},
};

interface HeroCardContent {
hero_card_title: string;
hero_card_description: string;
hero_card_link_text: string;
hero_card_link: string;
enum RequiredKeys {
hero_card_title = 'hero_card_title',
hero_card_description = 'hero_card_description',
hero_card_link_text = 'hero_card_link_text',
hero_card_link = 'hero_card_link',
}
// Content if the CMS fails - default to this
const heroCardFallbackContent: HeroCardContent = {
hero_card_title: `Latest blog`,
hero_card_description: `How an audio player component tells the story of NewsKit Design System's changing strategy.`,
hero_card_link_text: `Read on Medium`,
hero_card_link: `https://medium.com/newskit-design-system/how-an-audio-player-component-tells-the-story-of-newskit-design-systems-changing-strategy-8dc99d37ed67`,
};

type HeroCardContent = PageCMSRequiredProps<RequiredKeys>;

const Index = ({
releases,
Expand Down Expand Up @@ -159,6 +154,6 @@ export async function getStaticProps() {
releases = releasesOrError;
}

const content = {...heroCardFallbackContent, ...formatSheetData(data)};
const content = parseCMSResponse(data, {required: RequiredKeys});
return {props: {releases, content}};
}
35 changes: 18 additions & 17 deletions site/utils/google-sheet/__tests__/get-sheets.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import {google} from 'googleapis';
import {getSheets} from '../get-sheets';
import {CMSError} from '../utils';

const homePageContent = [
['hero_card_link_text', 'Read on Medium'],
['hero_card_link', 'https://medium.com/test'],
];

// The google.sheets() constructor returns a sheets function instance
const mockGoogleSheets = (values: string[][]) => () => ({
const mockGoogleSheets = (values?: string[][]) => () => ({
spreadsheets: {
values: {
get: jest.fn(() => ({
data: {
values: Promise.resolve(values),
},
})),
get: jest.fn(() =>
values
? Promise.resolve({
data: {
values,
},
})
: Promise.reject(new Error('Google Sheets error')),
),
},
},
});
Expand Down Expand Up @@ -55,22 +60,18 @@ describe('getSheets', () => {
process.env.GOOGLE_SHEETS_CLIENT_EMAIL = undefined;
process.env.GOOGLE_SHEETS_PRIVATE_KEY = undefined;
process.env.SPREADSHEET_ID = undefined;
const result = await getSheets('Homepage');
expect(googleSheetsSpy).not.toBeCalled();
expect(consoleErrorSpy).toBeCalled();
expect(result).toEqual([]);
await expect(async () => {
await getSheets('Homepage');
}).rejects.toThrowError(new CMSError('Missing environment variables'));
});

it('should return empty array on google failure', async () => {
process.env.GOOGLE_SHEETS_CLIENT_EMAIL = 'test';
process.env.GOOGLE_SHEETS_PRIVATE_KEY = 'test';
process.env.SPREADSHEET_ID = 'test';
googleSheetsSpy.mockImplementation(() => () =>
Promise.reject(new Error('Google error')),
);
const result = await getSheets('Homepage');
expect(googleSheetsSpy).toBeCalled();
expect(consoleErrorSpy).toBeCalled();
expect(result).toEqual([]);
googleSheetsSpy.mockImplementation(mockGoogleSheets());
await expect(async () => {
await getSheets('Homepage');
}).rejects.toThrowError(new CMSError('Google Sheets error'));
});
});
73 changes: 70 additions & 3 deletions site/utils/google-sheet/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {formatSheetData, getCMSList} from '../utils';
import {
CMSError,
formatSheetData,
getCMSPropsWithPrefix,
parseCMSResponse,
} from '../utils';

describe('formatSheetData', () => {
it('should convert google sheet data to an object', () => {
Expand All @@ -21,7 +26,7 @@ describe('formatSheetData', () => {
});
});

describe('getCMSList', () => {
describe('getCMSPropsWithPrefix', () => {
it('should return an array of items starting with a key', () => {
const values = [
['foo_li_1', 'foo1'],
Expand All @@ -31,11 +36,73 @@ describe('getCMSList', () => {
['baz', 'baz'],
];
const content = formatSheetData(values);
const result = getCMSList(content, 'foo_li');
const result = getCMSPropsWithPrefix(content, 'foo_li');
expect(result).toEqual([
['foo_li_1', 'foo1'],
['foo_li_2', 'foo2'],
['foo_li_3', 'foo3'],
]);
});
});

describe('parseCMSResponse', () => {
it('should throw an error if the response is empty', () => {
const response = null;
expect(() => {
parseCMSResponse(response, {required: {}});
}).toThrowError(new CMSError('No CMS data found'));
});
it('should throw an error if there are missing keys', () => {
const response: string[][] = [];
expect(() => {
parseCMSResponse(response, {
required: {
key_1: 'key_1',
},
});
}).toThrowError(new CMSError('MISSING_KEYS: key_1. '));
});
it('should throw an error if there are invalid keys', () => {
const response = [
['key_1', 'value_1'],
['key_2', 'value_2'],
];
expect(() => {
parseCMSResponse(response, {
required: {
key_1: 'key_1',
},
});
}).toThrowError(new CMSError('INVALID_KEYS: key_2. '));
});
it('should throw an error if there are missing and invalid keys', () => {
const response = [['key_2', 'value_2']];
expect(() => {
parseCMSResponse(response, {
required: {
key_1: 'key_1',
},
});
}).toThrowError(new CMSError('MISSING_KEYS: key_1. INVALID_KEYS: key_2. '));
});
it('should return the parsed content if all keys are valid', () => {
const response = [
['required_key', 'required_value'],
['dynamic_key_0', 'dynamic_value_0'],
['dynamic_key_1', 'dynamic_value_1'],
];
const parsed = parseCMSResponse(response, {
required: {
required_key: 'required_key',
},
dynamic: {
dynamic_key_: 'dynamic_key_',
},
});
expect(parsed).toEqual({
required_key: 'required_value',
dynamic_key_0: 'dynamic_value_0',
dynamic_key_1: 'dynamic_value_1',
});
});
});
14 changes: 3 additions & 11 deletions site/utils/google-sheet/get-sheets.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
/* eslint-disable no-console */
import {google} from 'googleapis';
import {CMSError} from './utils';

require('dotenv').config();

// Define the required scopes. In our case we only need read access.
const SCOPE = ['https://www.googleapis.com/auth/spreadsheets.readonly'];

const handleError = (err: unknown) => {
console.error('>> ERROR: Cannot fetch data from googlesheet API.', err);
};

// Range is typically the sheet name
export async function getSheets(range: string) {
const {
Expand All @@ -22,9 +18,7 @@ export async function getSheets(range: string) {
!GOOGLE_SHEETS_PRIVATE_KEY &&
!SPREADSHEET_ID
) {
// Fail fast without a stacktrace
handleError('Have you added the .env file for local builds?');
return [];
throw new CMSError('Missing environment variables');
}

try {
Expand All @@ -46,8 +40,6 @@ export async function getSheets(range: string) {
});
return response.data.values;
} catch (err: unknown) {
handleError(err);
throw new CMSError(err instanceof Error ? err.message : 'Unknown error');
}

return [];
}
16 changes: 12 additions & 4 deletions site/utils/google-sheet/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
export interface CMSDataProps {
export type PageCMSRequiredProps<T extends string> = {
[key in T]: string;
};

export type PageCMSPrefixedProps<T extends string> = {
[key in `${T}${number}`]: string;
};

export interface CMSProps {
[key: string]: string;
}

export interface ContentProps {
content: CMSDataProps;
}
export type CMSData = string[][];

export type CMSResponse = CMSData | null | undefined;