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

Adding Simple URL-based feature flags #117

Merged
merged 4 commits into from
Jun 9, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions client/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@
"stopOnEntry": false,
"runtimeArgs": ["--nolazy"],
"sourceMaps": false
},
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"--inspect",
"${workspaceRoot}/node_modules/.bin/jest"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
}
]
}
43 changes: 31 additions & 12 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
},
"dependencies": {
"@trussworks/react-uswds": "github:nathillardusds/react-uswds#nathillardusds/ssr",
"query-string": "^7.0.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-helmet": "^6.1.0",
Expand Down
18 changes: 13 additions & 5 deletions client/src/components/J40Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@ import {GovBanner,
Title,
PrimaryNav,
SiteAlert} from '@trussworks/react-uswds';
import {useIntl} from 'gatsby-plugin-intl';
import {useIntl, Link} from 'gatsby-plugin-intl';
import {Helmet} from 'react-helmet';
const headerLinks = [
<></>,
];
import {useFlags} from '../contexts/FlagContext';

const headerLinks = (flags: string[] | undefined) => {
const timelineLink = <Link key="/timeline" to="/timeline"> Timeline </Link>;
const links = [];
if (flags && flags.includes('timeline')) {
links.push(timelineLink);
}
return links;
};

const J40Header = () => {
const flags = useFlags();
const intl = useIntl();
const title = intl.formatMessage({
id: '71L0pp',
Expand Down Expand Up @@ -39,7 +47,7 @@ const J40Header = () => {
<div className="usa-navbar">
<Title className={'j40-title'}>{title}</Title>
</div>
<PrimaryNav items={headerLinks}/>
<PrimaryNav items={headerLinks(flags)} />
</div>
</Header>
</>
Expand Down
19 changes: 12 additions & 7 deletions client/src/components/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@ import React, {ReactNode} from 'react';
import * as styles from './layout.module.scss';
import J40Header from './J40Header';
import J40Footer from './J40Footer';
import {URLFlagProvider} from '../contexts/FlagContext';

interface ILayoutProps {
children: ReactNode
children: ReactNode,
location: URL
}

const Layout = ({children}: ILayoutProps) => {
const Layout = ({children, location}: ILayoutProps) => {
return (
<div className={styles.site}>
<J40Header />
<div className={styles.siteContent}>{children}</div>
<J40Footer />
</div>
<URLFlagProvider location={location}>
<div className={styles.site}>
<J40Header />
<div className={styles.siteContent}>{children}</div>
<J40Footer />
</div>
</URLFlagProvider>
);
};

Expand Down
39 changes: 39 additions & 0 deletions client/src/contexts/FlagContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react';
import {render, screen} from '@testing-library/react';
import {URLFlagProvider, useFlags} from './FlagContext';

describe('URL params are parsed and passed to children', () => {
describe('when the URL has a "flags" parameter set', () => {
// We artificially set the URL to localhost?flags=1,2,3
beforeEach(() => {
window.history.pushState({}, 'Test Title', '/?flags=1,2,3');
});
describe('when using useFlags', () => {
beforeEach(() => {
const FlagConsumer = () => {
const flags = useFlags();
return (
<>
<div>{flags.includes('1') ? 'yes1' : 'no1'}</div>
<div>{flags.includes('2') ? 'yes2' : 'no2'}</div>
<div>{flags.includes('3') ? 'yes3' : 'no3'}</div>
<div>{flags.includes('4') ? 'yes4' : 'no4'}</div>
</>
);
};
render(
<URLFlagProvider location={location}>
<FlagConsumer />
</URLFlagProvider>,
);
});

it('gives child components the flag values', async () => {
expect(screen.queryByText('yes1')).toBeInTheDocument();
expect(screen.queryByText('yes2')).toBeInTheDocument();
expect(screen.queryByText('yes3')).toBeInTheDocument();
expect(screen.queryByText('yes4')).not.toBeInTheDocument();
});
});
});
});
56 changes: 56 additions & 0 deletions client/src/contexts/FlagContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from 'react';
import * as queryString from 'query-string';

/**
* FlagContext stores feature flags and passes them to consumers
*/
interface IFlagContext {
/**
* Contains a list of all currently-active flags
*/
flags: string[];
}

const FlagContext = React.createContext<IFlagContext>({flags: []});

/**
* `useFlags` returns all feature flags.
*
* @return {Flags[]} flags All project feature flags
*/
const useFlags = () : string[] => {
const {flags} = React.useContext(FlagContext);
return flags;
};

interface IURLFlagProviderProps {
children: React.ReactNode,
location: Location
}

/**
* `URLFlagProvider` is a provider for FlagContext.
* It is passed the current URL and parses the
* "flags" parameter, assumed to be a comma-separated
* list of currently-active flags.
* @param {URL} location : the current URL object
* @param {ReactNode} children : the children components
* @return {ReactNode} URLFlagProvider component
**/
const URLFlagProvider = ({children, location}: IURLFlagProviderProps) => {
const flagString = queryString.parse(location.search).flags;
let flags: string[] = [];
if (flagString && typeof flagString === 'string') {
flags = (flagString as string).split(',');
}
console.log(JSON.stringify(location), JSON.stringify(flags));

return (
<FlagContext.Provider
value={{flags}}>
{children}
</FlagContext.Provider>
);
};

export {FlagContext, URLFlagProvider, useFlags};
7 changes: 5 additions & 2 deletions client/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ import pollutionIcon // @ts-ignore
import washIcon from '/node_modules/uswds/dist/img/usa-icons/wash.svg';
import J40Aside from '../components/J40Aside';

interface IndexPageProps {
location: URL;
};

// markup
const IndexPage = () => {
const IndexPage = ({location}: IndexPageProps) => {
const readMoreList: (any | string)[][] = [
[ecoIcon, 'Clean energy and energy efficiency'],
[busIcon, 'Clean transit'],
Expand All @@ -33,7 +36,7 @@ const IndexPage = () => {
];

return (
<Layout>
<Layout location={location}>
<main id="main-content" role="main">
<div className={'grid-row grid-gap-2'}>
<section className={'grid-container usa-section'}>
Expand Down
8 changes: 6 additions & 2 deletions client/src/pages/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import J40Aside from '../components/J40Aside';
// @ts-ignore
import renewIcon from '/node_modules/uswds/dist/img/usa-icons/autorenew.svg';

const TimelinePage = () => {
return (<Layout>
interface TimelinePageProps {
location: URL;
};

const TimelinePage = ({location}: TimelinePageProps) => {
return (<Layout location={location}>
<main id="main-content" role="main">
<div className={'grid-row grid-gap-2'}>
<section className={'grid-container usa-section'}>
Expand Down
6 changes: 3 additions & 3 deletions client/src/styles/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@

.j40-address-readability {
display: inline-block;
line-height: 1.5 !important; // trussworks issue
line-height: 1.5 !important; // trussworks issue
text-align: center;
}

Expand All @@ -68,8 +68,8 @@
&::before {
color: white;
background-color: #00a91c;
content: '✓'
content: "✓";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think this is an indication that prettier isn't running correctly for my ide setup?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, that appears to have snuck in - this was formatted automatically when I ran gatsby develop

}

border-left-color: #005ea2 !important; // todo: fix
border-left-color: #005ea2 !important; // todo: fix
}