[next] Admin Configure (#2076)

* feat: Add RadioButton and CheckBox

* feat: configure facebook and google auth

* feat: configure sso, localAuth and displayName + some tests

* test: add integration tests for configure auth

* test: more integration tests

* feat: add oidc support

* test: add oidc integration test

* feat: generate sso key initially

* fix: import fetchQuery from correct package

* fix: admin url

* fix: set timezone to utc when testing

* refactor: improve route config

* fix: remove obsolete line

* fix: clientMutationId increment

* fix: oidc only create when enabled

* fix: copy

* test: update snapshots

* feat: fixed graphql logging extension

* Update src/locales/en-US/admin.ftl

Co-Authored-By: cvle <vinh@wikiwi.io>

* Apply suggestions from code review

Co-Authored-By: cvle <vinh@wikiwi.io>

* test: update snapshots

* fix: change Local Auth to Email Authentication

* fix: copy updates
This commit is contained in:
Kiwi
2018-11-19 22:47:32 +00:00
committed by Wyatt Johnson
parent 3f949b3712
commit 05350d651f
215 changed files with 7774 additions and 939 deletions
@@ -0,0 +1,62 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import CopyToClipboard from "react-copy-to-clipboard";
import { Button } from "talk-ui/components";
import { PropTypesOf } from "talk-ui/types";
interface InnerProps extends PropTypesOf<typeof Button> {
text: string;
}
interface State {
copied: boolean;
}
class PermalinkPopover extends React.Component<InnerProps> {
private timeout: any = null;
public state: State = {
copied: false,
};
public componentWillUnmount() {
clearTimeout(this.timeout);
}
private handleCopy = () => {
this.setCopied(true);
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.setCopied(false);
}, 500);
};
private setCopied = (b: boolean) => {
this.setState({
copied: b,
});
};
public render() {
const { text, ...rest } = this.props;
const { copied } = this.state;
return (
<CopyToClipboard text={text} onCopy={this.handleCopy}>
<Button color="primary" variant="filled" size="small" {...rest}>
{copied ? (
<Localized id="framework-copyButton-copied">
<span>Copied!</span>
</Localized>
) : (
<Localized id="framework-copyButton-copy">
<span>Copy</span>
</Localized>
)}
</Button>
</CopyToClipboard>
);
}
}
export default PermalinkPopover;
@@ -0,0 +1 @@
export { default } from "./CopyButton";
@@ -0,0 +1 @@
export { default as CopyButton } from "./CopyButton";
@@ -0,0 +1,36 @@
import * as React from "react";
import {
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
/**
* withContext is a HOC wrapper around `TalkContextConsumer`.
* `propsCallback` must be provided which accepts the `TalkContext`
* and returns the props the should be injected.
*/
function createContextHOC<Context>(
displayName: string,
Consumer: React.ComponentType<React.ConsumerProps<Context>>
) {
return function withContext<T>(
propsCallback: (context: Context) => T
): InferableComponentEnhancer<T> {
return hoistStatics<T>(
<U extends T>(WrappedComponent: React.ComponentType<U>) => {
const Component: React.StatelessComponent<any> = props => (
<Consumer>
{context => (
<WrappedComponent {...props} {...propsCallback(context)} />
)}
</Consumer>
);
Component.displayName = wrapDisplayName(WrappedComponent, displayName);
return Component;
}
);
};
}
export default createContextHOC;
@@ -2,3 +2,4 @@ export { default as getMe } from "./getMe";
export { default as getMeSourceID } from "./getMeSourceID";
export { default as getURLWithCommentID } from "./getURLWithCommentID";
export { default as urls } from "./urls";
export { default as createContextHOC } from "./createContextHOC";
+1 -1
View File
@@ -7,7 +7,7 @@ export default (process.env.NODE_ENV !== "development"
},
}
: {
admin: "/admin.html",
admin: "/admin",
embed: {
stream: "/stream.html",
auth: "/auth.html",
@@ -1,33 +1,9 @@
import * as React from "react";
import {
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
import { createContextHOC } from "talk-framework/helpers";
import { TalkContext, TalkContextConsumer } from "./TalkContext";
/**
* withContext is a HOC wrapper around `TalkContextConsumer`.
* `propsCallback` must be provided which accepts the `TalkContext`
* and returns the props the should be injected.
*/
function withContext<T>(
propsCallback: (context: TalkContext) => T
): InferableComponentEnhancer<T> {
return hoistStatics<T>(
<U extends T>(WrappedComponent: React.ComponentType<U>) => {
const Component: React.StatelessComponent<any> = props => (
<TalkContextConsumer>
{context => (
<WrappedComponent {...props} {...propsCallback(context)} />
)}
</TalkContextConsumer>
);
Component.displayName = wrapDisplayName(WrappedComponent, "withContext");
return Component;
}
);
}
const withContext = createContextHOC<TalkContext>(
"withContext",
TalkContextConsumer
);
export default withContext;
@@ -0,0 +1,17 @@
import { FluentBundle } from "fluent/compat";
export default function getMessage(
bundles: FluentBundle[],
key: string,
defaultTo = ""
): string {
const res = bundles.reduce((val, bundle) => {
const got = bundle.getMessage(key);
if (!got && process.env.NODE_ENV !== "production") {
// tslint:disable-next-line:no-console
console.warn(`Translation ${key} was not found for ${bundle.locales}`);
}
return val || got;
}, "");
return res || defaultTo;
}
@@ -1,3 +1,4 @@
export { default as generateBundles } from "./generateBundles";
export { default as negotiateLanguages } from "./negotiateLanguages";
export { BundledLocales, LoadableLocales, LocalesData } from "./locales";
export { default as getMessage } from "./getMessage";
@@ -3,6 +3,8 @@ import { Environment, MutationConfig, OperationBase } from "relay-runtime";
import { Omit } from "talk-framework/types";
import extractPayload from "./extractPayload";
/**
* Like `MutationConfig` but omits `onCompleted` and `onError`
* because we are going to use a Promise API.
@@ -12,15 +14,6 @@ export type MutationPromiseConfig<T extends OperationBase> = Omit<
"onCompleted" | "onError"
>;
// Extract the payload from the response,
function getPayload(response: { [key: string]: any }): any {
const keys = Object.keys(response);
if (keys.length !== 1) {
return response;
}
return response[keys[0]];
}
/**
* Normalizes response and error from `commitMutationPromise`.
* Meaning `response` will directly contain the payload
@@ -33,7 +26,7 @@ export async function commitMutationPromiseNormalized<T extends OperationBase>(
): Promise<T["response"][keyof T["response"]]> {
try {
const response = await commitMutationPromise(environment, config);
return getPayload(response);
return extractPayload(response);
} catch (e) {
throw e;
}
@@ -57,7 +50,7 @@ export function commitMutationPromise<T extends OperationBase>(
reject(errors);
return;
}
resolve(getPayload(response));
resolve(extractPayload(response));
},
onError: error => {
reject(error);
@@ -0,0 +1,56 @@
import * as React from "react";
import {
compose,
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
import { Environment } from "relay-runtime";
import { TalkContext, withContext } from "../bootstrap";
/**
* createFetchContainer creates a HOC that
* injects a property with the name specified in `propName`
* and the signature (input: I) => Promise<R>. Calling
* this will start a one off query.
*/
function createFetchContainer<T extends string, V, R>(
propName: T,
fetch: (
environment: Environment,
variables: V,
context: TalkContext
) => Promise<R>
): InferableComponentEnhancer<{ [P in T]: (variables: V) => Promise<R> }> {
return compose(
withContext(context => ({ context })),
hoistStatics((BaseComponent: React.ComponentType<any>) => {
class CreateFetchContainer extends React.Component<any> {
public static displayName = wrapDisplayName(
BaseComponent,
"createFetchContainer"
);
private fetch = (variables: V) => {
return fetch(
this.props.context.relayEnvironment,
variables,
this.props.context
);
};
public render() {
const { context: _, ...rest } = this.props;
const inject = {
[propName]: this.fetch,
};
return <BaseComponent {...rest} {...inject} />;
}
}
return CreateFetchContainer as React.ComponentType<any>;
})
);
}
export default createFetchContainer;
@@ -0,0 +1,8 @@
// Extract the payload from the response,
export default function extractPayload(response: { [key: string]: any }): any {
const keys = Object.keys(response);
if (keys.length !== 1) {
return response;
}
return response[keys[0]];
}
@@ -0,0 +1,24 @@
import { fetchQuery as relayFetchQuery } from "react-relay";
import {
CacheConfig,
Environment,
GraphQLTaggedNode,
Variables,
} from "relay-runtime";
import extractPayload from "./extractPayload";
export default async function fetchQuery<R = any>(
environment: Environment,
taggedNode: GraphQLTaggedNode,
variables: Variables,
cacheConfig?: CacheConfig
): Promise<R> {
const result = await relayFetchQuery(
environment,
taggedNode,
variables,
cacheConfig
);
return extractPayload(result);
}
@@ -6,6 +6,7 @@ export * from "./withLocalStateContainer";
export { default as QueryRenderer } from "./QueryRenderer";
export * from "./QueryRenderer";
export { default as createMutationContainer } from "./createMutationContainer";
export { default as createFetchContainer } from "./createFetchContainer";
export { default as createAndRetain } from "./createAndRetain";
export { default as wrapFetchWithLogger } from "./wrapFetchWithLogger";
export {
@@ -17,3 +18,4 @@ export {
default as commitLocalUpdatePromisified,
} from "./commitLocalUpdatePromisified";
export { initLocalBaseState, setAuthTokenInLocalState } from "./localState";
export { default as fetchQuery } from "./fetchQuery";
+2 -2
View File
@@ -10,7 +10,7 @@ import {
VALIDATION_REQUIRED,
} from "./messages";
type Validator<T, V> = (v: T, values: V) => ReactNode;
export type Validator<T = any, V = any> = (v: T, values: V) => ReactNode;
/**
* createValidator returns a Validator that returns given `error` when `condition` is falsey.
@@ -62,7 +62,7 @@ export const validateUsernameCharacters = createValidator(
*/
export const validateURL = createValidator(
v =>
/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/.test(
/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)[a-zA-Z0-9]+([\-\.]{1}[a-zA-Z0-9]+)*\.[a-zA-Z]{2,5}(:[0-9]{1,5})?(\/.*)?$/.test(
v
),
INVALID_URL()
@@ -11,3 +11,5 @@ export {
export { default as createUUIDGenerator } from "./createUUIDGenerator";
export * from "./denormalize";
export { default as replaceHistoryLocation } from "./replaceHistoryLocation";
export { default as limitSnapshotTo } from "./limitSnapshotTo";
export { default as inputPredicate } from "./inputPredicate";
@@ -0,0 +1,10 @@
import { ReactTestInstance } from "react-test-renderer";
const inputPredicate = (nameOrID: string) => (n: ReactTestInstance) => {
return (
[n.props.name, n.props.id].indexOf(nameOrID) > -1 &&
["input", "button"].indexOf(n.type) > -1
);
};
export default inputPredicate;
@@ -0,0 +1,14 @@
export default function limitSnapshotTo(id: string, node: any) {
if (node.props && node.props.id === id) {
return node;
}
if (node.children) {
for (const child of node.children) {
const result: any = limitSnapshotTo(id, child);
if (result) {
return result;
}
}
}
return "";
}