Skip to content

Commit

Permalink
feat: add db-sso
Browse files Browse the repository at this point in the history
  • Loading branch information
jrea committed May 4, 2023
1 parent 375c06b commit 04ae56e
Show file tree
Hide file tree
Showing 18 changed files with 242 additions and 38 deletions.
2 changes: 2 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@svgr/webpack": "^5.5.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@types/js-cookie": "^3.0.3",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"@typescript-eslint/parser": "^5.49.0",
Expand Down Expand Up @@ -91,6 +92,7 @@
"chart.js": "^3.9.1",
"chartjs-adapter-date-fns": "^2.0.1",
"date-fns": "^2.29.3",
"js-cookie": "^3.0.5",
"react": "^18.2.0",
"react-chartjs-2": "^4.3.1",
"react-dom": "^18.2.0",
Expand Down
84 changes: 84 additions & 0 deletions packages/react/src/SignUpForm/SignUpForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';
import { useMutation } from '@tanstack/react-query';
import Cookies from 'js-cookie';

import UserForm from '../lib/SimpleForm';
import { Attribute, AttributeType } from '../lib/SimpleForm/types';
import { useNileConfig } from '../context';

import { Props } from './types';

export default function SignUpForm(props: Props) {
const { buttonText = 'Sign up', onSuccess, onError, attributes } = props;
const { workspace, database, basePath, allowClientCookies } = useNileConfig();
const fetchPath = `${basePath}/workspaces/${workspace}/databases/${database}/users`;

const mutation = useMutation(
async (data: { email: string; password: string }) => {
const { email, password, ...metadata } = data;
if (Object.keys(metadata).length > 0) {
// eslint-disable-next-line no-console
console.warn('additional metadata not supported yet.');
}

const res = await fetch(fetchPath, {
method: 'POST',
body: JSON.stringify({ email, password }),
headers: {
'content-type': 'application/json',
},
}).catch((e) => e);
try {
if (res) {
if (allowClientCookies) {
Cookies.set('token', res.token.token, {
'max-age': res.token.maxAge,
});
}
return await res.json();
}
} catch (e) {
return e;
}
},
{
onSuccess: (_, data) => {
onSuccess && onSuccess(data);
},
onError: (error, data) => {
onError && onError(error as Error, data);
},
}
);

const completeAttributes = React.useMemo(() => {
const mainAttributes: Attribute[] = [
{
name: 'email',
label: 'Email',
type: AttributeType.Text,
defaultValue: '',
required: true,
},
{
name: 'password',
label: 'Password',
type: AttributeType.Password,
defaultValue: '',
required: true,
},
];
if (attributes && attributes.length > 0) {
return mainAttributes.concat(attributes);
}
return mainAttributes;
}, [attributes]);

return (
<UserForm
mutation={mutation}
buttonText={buttonText}
attributes={completeAttributes}
/>
);
}
1 change: 1 addition & 0 deletions packages/react/src/SignUpForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './SignUpForm';
11 changes: 11 additions & 0 deletions packages/react/src/SignUpForm/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Attribute } from '../lib/SimpleForm/types';

type LoginInfo = { email: string; password: string };
type SignInSuccess = (loginInfo: LoginInfo) => void;

export interface Props {
onSuccess: SignInSuccess;
onError?: (e: Error, info: LoginInfo) => void;
attributes?: Attribute[];
buttonText?: string;
}
11 changes: 8 additions & 3 deletions packages/react/src/context/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const defaultContext: NileContext = {
workspace: '',
database: '',
basePath: '',
allowClientCookies: true, // totally insecure, but makes it easy for getting started
};

const context = createContext<NileContext>(defaultContext);
Expand All @@ -44,6 +45,7 @@ export const NileProvider = (props: NileProviderProps) => {
database,
tokenStorage,
QueryProvider = BaseQueryProvider,
allowClientCookies,
basePath = 'https://prod.thenile.dev',
} = props;

Expand All @@ -58,8 +60,9 @@ export const NileProvider = (props: NileProviderProps) => {
workspace: String(workspace),
database: String(database),
basePath,
allowClientCookies,
};
}, [basePath, database, tokenStorage, workspace]);
}, [basePath, database, tokenStorage, workspace, allowClientCookies]);

return (
<QueryProvider>
Expand All @@ -81,13 +84,15 @@ export const useNile = (): NileApi => {
};

export const useNileConfig = (): NileReactConfig => {
const { database, workspace, basePath } = useNileContext();
const { database, workspace, basePath, allowClientCookies } =
useNileContext();
return useMemo(
() => ({
workspace,
database,
basePath,
allowClientCookies,
}),
[basePath, database, workspace]
[allowClientCookies, basePath, database, workspace]
);
};
13 changes: 6 additions & 7 deletions packages/react/src/context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,19 @@ import { Theme } from '@mui/joy/styles';

export interface NileReactConfig {
workspace: string;
database: string;
basePath: string;
database?: string;
basePath?: string;
allowClientCookies?: boolean;
}

export type NileContext = NileReactConfig & {
instance: NileApi;
theme?: Theme;
};

export interface NileProviderProps {
export type NileProviderProps = NileReactConfig & {
children: JSX.Element | JSX.Element[];
basePath?: string;
workspace?: string;
database?: string;
theme?: Theme;
tokenStorage?: StorageOptions;
QueryProvider?: (props: { children: JSX.Element }) => JSX.Element;
}
};
9 changes: 7 additions & 2 deletions packages/react/src/lib/SimpleForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const getAttributeDefault = (
return attribute.defaultValue ?? '';
};

export default function ConfigForm(props: {
export default function SimpleForm(props: {
buttonText: string;
cancelButton?: React.ReactNode;
attributes: Attribute[];
Expand Down Expand Up @@ -80,14 +80,15 @@ export default function ConfigForm(props: {
const display: DisplayProps = {
key: attr.name,
label: attr.label ?? attr.name,
id: attr.label ?? attr.name,
placeholder: attr.placeholder ?? attr.label ?? attr.name,
error: Boolean(errors[attr.name]),
};
const options = attr.options ?? [];
let helperText = '';

if (attr.required) {
helperText = errors[attr.name] ? 'This field is required' : '';
helperText = errors[attr.name] ? `${attr.name} is required` : '';
fieldConfig.required = true;
}

Expand All @@ -109,6 +110,7 @@ export default function ConfigForm(props: {
sx={{
'--FormHelperText-color': 'var(--joy-palette-danger-500)',
}}
id={display.id}
>
<FormLabel htmlFor={`select-field-${attr.name}`}>
{display.label}
Expand Down Expand Up @@ -157,6 +159,7 @@ export default function ConfigForm(props: {
sx={{
'--FormHelperText-color': 'var(--joy-palette-danger-500)',
}}
id={display.id}
>
<FormLabel>{attr.label ?? attr.name}</FormLabel>
<Input
Expand All @@ -176,6 +179,7 @@ export default function ConfigForm(props: {
sx={{
'--FormHelperText-color': 'var(--joy-palette-danger-500)',
}}
id={display.id}
>
<FormLabel>{attr.label ?? attr.name}</FormLabel>
<Input
Expand All @@ -197,6 +201,7 @@ export default function ConfigForm(props: {
sx={{
'--FormHelperText-color': 'var(--joy-palette-danger-500)',
}}
id={display.id}
>
<FormLabel>{attr.label ?? attr.name}</FormLabel>
<Input {...display} {...register(attr.name, fieldConfig)} />
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/lib/SimpleForm/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type Attribute = {

export type DisplayProps = {
key: string;
id: string;
label: string;
placeholder: string;
error?: boolean;
Expand Down
10 changes: 5 additions & 5 deletions packages/react/stories/InstanceList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ type StoryProps = {

const Template: Story<StoryProps> = (args) => {
return (
<NileProvider basePath="http://localhost:8080">
<NileProvider basePath="http://localhost:8080" workspace="workspace">
<InstanceList
entity="myEntity"
isFetching={args.isFetching}
Expand All @@ -121,7 +121,7 @@ Default.args = {
};

const EmptyState = () => (
<NileProvider basePath="http://localhost:8080">
<NileProvider basePath="http://localhost:8080" workspace="workspace">
<InstanceList
entity="myEntity"
isFetching={false}
Expand Down Expand Up @@ -167,7 +167,7 @@ const RenderEventLink = (props: GridRenderCellParams<Instance>) => {
};

const Columns = () => (
<NileProvider basePath="http://localhost:8080">
<NileProvider basePath="http://localhost:8080" workspace="workspace">
<InstanceList
entity="myEntity"
isFetching={false}
Expand All @@ -191,7 +191,7 @@ const Columns = () => (
export const FilteredAndCustomColumns = Columns.bind({});

const ActionButtons = () => (
<NileProvider basePath="http://localhost:8080">
<NileProvider basePath="http://localhost:8080" workspace="workspace">
<InstanceList
entity="myEntity"
isFetching={false}
Expand All @@ -215,7 +215,7 @@ const ActionButtons = () => (
export const TableActionButtons = ActionButtons.bind({});

const Cards = () => (
<NileProvider basePath="http://localhost:8080">
<NileProvider basePath="http://localhost:8080" workspace="workspace">
<InstanceList
entity="myEntity"
isFetching={false}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/stories/OrganizationForm.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const meta = {
export default meta;

const Template: Story = () => (
<NileProvider basePath="http://localhost:8080">
<NileProvider basePath="http://localhost:8080" workspace="workspace">
<div style={{ maxWidth: '20rem', margin: '0 auto' }}>
<OrganizationForm
onSuccess={() => alert('success!')}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/stories/SSO/GoogleLoginButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const meta = {
export default meta;

const Template: Story = () => (
<NileProvider basePath="http://localhost:8080" workspace="my_workspace">
<NileProvider database="database" workspace="my_workspace">
<div style={{ maxWidth: '20rem', margin: '0 auto' }}>
<GoogleLoginButton />
</div>
Expand Down
26 changes: 26 additions & 0 deletions packages/react/stories/SignUpForm/NewUserSignUp.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { Meta, Story } from '@storybook/react';

import { NileProvider } from '../../src/context';
import SignUpForm from '../../src/SignUpForm';

const meta: Meta = {
component: SignUpForm,
parameters: {
controls: { expanded: false },
},
};

export default meta;

const Template: Story<null> = () => (
<NileProvider workspace="workspace" database="database">
<div style={{ maxWidth: '20rem', margin: '0 auto' }}>
<SignUpForm onSuccess={() => alert('success!')} />
</div>
</NileProvider>
);

// By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
// https://storybook.js.org/docs/react/workflows/unit-testing
export const Default = Template.bind({});
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import { Meta, Story } from '@storybook/react';

import SignUpForm from '../src/components/SignUpForm';
import { NileProvider } from '../src/context';
import SignUpForm from '../../src/components/SignUpForm';
import { NileProvider } from '../../src/context';

const meta: Meta = {
component: SignUpForm,
Expand All @@ -21,15 +21,15 @@ const meta: Meta = {
export default meta;

const Template: Story<null> = () => (
<NileProvider basePath="http://localhost:8080">
<NileProvider basePath="http://localhost:8080" workspace="workspace">
<div style={{ maxWidth: '20rem', margin: '0 auto' }}>
<SignUpForm onSuccess={() => alert('success!')} />
</div>
</NileProvider>
);

export const CustomFields: Story<null> = () => (
<NileProvider basePath="http://localhost:8080">
<NileProvider basePath="http://localhost:8080" workspace="workspace">
<div style={{ maxWidth: '20rem', margin: '0 auto' }}>
<SignUpForm
onSuccess={() => alert('success!')}
Expand Down
16 changes: 1 addition & 15 deletions packages/react/test/GoogleLoginButton/GoogleSSOButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,10 @@ import { render, screen } from '@testing-library/react';

import GoogleSSOButton from '../../src/GoogleLoginButton/GoogleSSOButton';
import { NileProvider } from '../../src/context';
import '../matchMedia.mock';

jest.mock('../../src/GoogleLoginButton/google.svg', () => 'svg');

// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});

describe('google sso button', () => {
it('renders with an href prop', () => {
const ref = 'somehref';
Expand Down
Loading

0 comments on commit 04ae56e

Please sign in to comment.