[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 23:47:32 +01:00
committed by Wyatt Johnson
parent 3f949b3712
commit 05350d651f
215 changed files with 7774 additions and 939 deletions
+7 -5
View File
@@ -10712,11 +10712,8 @@
"resolved": "https://registry.npmjs.org/fluent-intl-polyfill/-/fluent-intl-polyfill-0.1.0.tgz",
"integrity": "sha1-ETOUSrJHeINHOZVZaIPg05z4hc8=",
"dev": true,
"dependencies": {
"intl-pluralrules": {
"version": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b",
"from": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b"
}
"requires": {
"intl-pluralrules": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b"
}
},
"fluent-langneg": {
@@ -13384,6 +13381,11 @@
"integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=",
"dev": true
},
"intl-pluralrules": {
"version": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b",
"from": "github:projectfluent/IntlPluralRules#module",
"dev": true
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+1
View File
@@ -233,6 +233,7 @@
"postcss-prepend-imports": "^1.0.1",
"postcss-preset-env": "^5.2.1",
"prettier": "^1.13.7",
"prop-types": "^15.6.2",
"pstree.remy": "^1.1.0",
"pym.js": "^1.3.2",
"query-string": "^6.1.0",
+2
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env node
"use strict";
// Set timezone to UTC for stable tests.
process.env.TZ = "UTC";
// Allow importing typescript files.
require("ts-node/register");
@@ -0,0 +1,39 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import { DiscoverOIDCConfigurationQuery as QueryTypes } from "talk-admin/__generated__/DiscoverOIDCConfigurationQuery.graphql";
import { createFetchContainer, fetchQuery } from "talk-framework/lib/relay";
export type DiscoverOIDCConfigurationVariables = QueryTypes["variables"];
const query = graphql`
query DiscoverOIDCConfigurationQuery($issuer: String!) {
discoverOIDCConfiguration(issuer: $issuer) {
issuer
authorizationURL
tokenURL
jwksURI
}
}
`;
function fetch(
environment: Environment,
variables: DiscoverOIDCConfigurationVariables
) {
return fetchQuery<QueryTypes["response"]["discoverOIDCConfiguration"]>(
environment,
query,
variables,
{ force: true }
);
}
export const withDiscoverOIDCConfigurationFetch = createFetchContainer(
"discoverOIDCConfiguration",
fetch
);
export type DiscoverOIDCConfigurationFetch = (
variables: DiscoverOIDCConfigurationVariables
) => Promise<QueryTypes["response"]["discoverOIDCConfiguration"]>;
+4
View File
@@ -0,0 +1,4 @@
export {
withDiscoverOIDCConfigurationFetch,
DiscoverOIDCConfigurationFetch,
} from "./DiscoverOIDCConfigurationQuery";
@@ -0,0 +1,71 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { CreateOIDCAuthIntegrationMutation as MutationTypes } from "talk-admin/__generated__/CreateOIDCAuthIntegrationMutation.graphql";
export type CreateOIDCAuthIntegrationInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation CreateOIDCAuthIntegrationMutation(
$input: CreateOIDCAuthIntegrationInput!
) {
createOIDCAuthIntegration(input: $input) {
settings {
auth {
...OIDCConfigListContainer_authReadOnly
}
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(
environment: Environment,
input: CreateOIDCAuthIntegrationInput
) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
updater: store => {
const record = store
.getRootField("createOIDCAuthIntegration")!
.getLinkedRecord("settings")!
.getLinkedRecord("auth")!
.getLinkedRecord("integrations");
if (record) {
store
.getRoot()
.getLinkedRecord("settings")!
.getLinkedRecord("auth")!
.getLinkedRecord("integrations")!
.copyFieldsFrom(record);
}
},
});
}
export const withCreateOIDCAuthIntegrationMutation = createMutationContainer(
"createOIDCAuthIntegration",
commit
);
export type CreateOIDCAuthIntegrationMutation = (
input: CreateOIDCAuthIntegrationInput
) => Promise<MutationTypes["response"]["createOIDCAuthIntegration"]>;
@@ -0,0 +1,66 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { RegenerateSSOKeyMutation as MutationTypes } from "talk-admin/__generated__/RegenerateSSOKeyMutation.graphql";
const mutation = graphql`
mutation RegenerateSSOKeyMutation($input: RegenerateSSOKeyInput!) {
regenerateSSOKey(input: $input) {
settings {
auth {
integrations {
sso {
key
keyGeneratedAt
}
}
}
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(environment: Environment) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
clientMutationId: (clientMutationId++).toString(),
},
},
updater: store => {
const record = store
.getRootField("regenerateSSOKey")!
.getLinkedRecord("settings")!
.getLinkedRecord("auth")!
.getLinkedRecord("integrations")!
.getLinkedRecord("sso");
if (record) {
store
.getRoot()
.getLinkedRecord("settings")!
.getLinkedRecord("auth")!
.getLinkedRecord("integrations")!
.getLinkedRecord("sso")!
.copyFieldsFrom(record);
}
},
});
}
export const withRegenerateSSOKeyMutation = createMutationContainer(
"regenerateSSOKey",
commit
);
export type RegenerateSSOKeyMutation = () => Promise<
MutationTypes["response"]["regenerateSSOKey"]
>;
@@ -0,0 +1,56 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { UpdateOIDCAuthIntegrationMutation as MutationTypes } from "talk-admin/__generated__/UpdateOIDCAuthIntegrationMutation.graphql";
export type UpdateOIDCAuthIntegrationInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation UpdateOIDCAuthIntegrationMutation(
$input: UpdateOIDCAuthIntegrationInput!
) {
updateOIDCAuthIntegration(input: $input) {
settings {
auth {
...OIDCConfigListContainer_authReadOnly
}
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(
environment: Environment,
input: UpdateOIDCAuthIntegrationInput
) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
});
}
export const withUpdateOIDCAuthIntegrationMutation = createMutationContainer(
"updateOIDCAuthIntegration",
commit
);
export type UpdateOIDCAuthIntegrationMutation = (
input: UpdateOIDCAuthIntegrationInput
) => Promise<MutationTypes["response"]["updateOIDCAuthIntegration"]>;
@@ -0,0 +1,70 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { UpdateSettingsMutation as MutationTypes } from "talk-admin/__generated__/UpdateSettingsMutation.graphql";
export type UpdateSettingsInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation UpdateSettingsMutation($input: UpdateSettingsInput!) {
updateSettings(input: $input) {
settings {
auth {
...FacebookConfigContainer_auth
...FacebookConfigContainer_authReadOnly
...GoogleConfigContainer_auth
...GoogleConfigContainer_authReadOnly
...SSOConfigContainer_auth
...SSOConfigContainer_authReadOnly
...OIDCConfigListContainer_auth
...OIDCConfigListContainer_authReadOnly
...DisplayNamesConfigContainer_auth
}
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(environment: Environment, input: UpdateSettingsInput) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
updater: store => {
const record = store
.getRootField("updateSettings")!
.getLinkedRecord("settings");
if (record) {
store
.getRoot()
.getLinkedRecord("settings")!
.copyFieldsFrom(record);
}
},
});
}
export const withUpdateSettingsMutation = createMutationContainer(
"updateSettings",
commit
);
export type UpdateSettingsMutation = (
input: UpdateSettingsInput
) => Promise<MutationTypes["response"]["updateSettings"]>;
+17
View File
@@ -3,3 +3,20 @@ export {
withSetRedirectPathMutation,
SetRedirectPathMutation,
} from "./SetRedirectPathMutation";
export {
withUpdateSettingsMutation,
UpdateSettingsMutation,
UpdateSettingsInput,
} from "./UpdateSettingsMutation";
export {
withRegenerateSSOKeyMutation,
RegenerateSSOKeyMutation,
} from "./RegenerateSSOKeyMutation";
export {
withCreateOIDCAuthIntegrationMutation,
CreateOIDCAuthIntegrationMutation,
} from "./CreateOIDCAuthIntegrationMutation";
export {
withUpdateOIDCAuthIntegrationMutation,
UpdateOIDCAuthIntegrationMutation,
} from "./UpdateOIDCAuthIntegrationMutation";
+8 -2
View File
@@ -5,7 +5,9 @@ import App from "./components/App";
import RedirectAppContainer from "./containers/RedirectAppContainer";
import RedirectLoginContainer from "./containers/RedirectLoginContainer";
import Community from "./routes/community/components/Community";
import Configure from "./routes/configure/components/Configure";
import ConfigureMisc from "./routes/configure/components/Misc";
import ConfigureContainer from "./routes/configure/containers/ConfigureContainer";
import ConfigureAuthContainer from "./routes/configure/sections/auth/containers/AuthContainer";
import Login from "./routes/login/components/Login";
import Moderate from "./routes/moderate/components/Moderate";
import Stories from "./routes/stories/components/Stories";
@@ -18,7 +20,11 @@ export default makeRouteConfig(
<Route path="moderate" Component={Moderate} />
<Route path="community" Component={Community} />
<Route path="stories" Component={Stories} />
<Route path="configure" Component={Configure} />
<Route path="configure" Component={ConfigureContainer}>
<Redirect from="/" to="/admin/configure/auth" />
<Route path="auth" {...ConfigureAuthContainer.routeConfig} />
<Route path="misc" Component={ConfigureMisc} />
</Route>
</Route>
</Route>
<Route Component={RedirectAppContainer}>
@@ -0,0 +1,20 @@
.root {
border: 1px solid var(--palette-grey-lighter);
}
.title {
padding: calc(0.5 * var(--spacing-unit)) calc(1.5 * var(--spacing-unit));
background-color: var(--palette-text-primary);
color: var(--palette-text-light);
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-medium);
font-size: calc(18rem / var(--rem-base));
line-height: calc(20em / 18);
letter-spacing: calc(-0.1em / 18);
}
.content {
padding: calc(2 * var(--spacing-unit)) calc(3 * var(--spacing-unit));
}
@@ -0,0 +1,16 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import ConfigBox from "./ConfigBox";
it("renders correctly", () => {
const props: PropTypesOf<typeof ConfigBox> = {
topRight: <span>topRight</span>,
title: <span>title</span>,
children: "child",
};
const wrapper = shallow(<ConfigBox {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,29 @@
import React, { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import styles from "./ConfigBox.css";
interface Props {
id?: string;
topRight?: React.ReactElement<any>;
title?: React.ReactNode;
children: React.ReactNode;
}
const ConfigBox: StatelessComponent<Props> = ({
id,
title,
topRight,
children,
}) => (
<div className={styles.root} id={id}>
<Flex className={styles.title} justifyContent="space-between">
<div>{title}</div>
<div>{topRight}</div>
</Flex>
<div className={styles.content}>{children}</div>
</div>
);
export default ConfigBox;
@@ -1,9 +1,16 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Configure from "./Configure";
it("renders correctly", () => {
const wrapper = shallow(<Configure />);
const props: PropTypesOf<typeof Configure> = {
onSave: noop,
onChange: noop,
};
const wrapper = shallow(<Configure {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -1,10 +1,62 @@
import { FormApi, FormState } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { HorizontalGutter, Typography } from "talk-ui/components";
import { Form, FormSpy } from "react-final-form";
const Configure: StatelessComponent = ({ children }) => (
<HorizontalGutter>
<Typography variant="heading3">Configure</Typography>
</HorizontalGutter>
import { Button, HorizontalGutter } from "talk-ui/components";
import Layout from "./Layout";
import Main from "./Main";
import { Link, Navigation } from "./Navigation";
import SideBar from "./SideBar";
interface Props {
onSave: (settings: any, form: FormApi) => void;
onChange: (formState: FormState) => void;
}
const Configure: StatelessComponent<Props> = ({
onSave,
onChange,
children,
}) => (
<div id="configure-container">
<Form onSubmit={onSave}>
{({ handleSubmit, submitting, pristine, form }) => (
<form autoComplete="off" onSubmit={handleSubmit} id="configure-form">
<FormSpy onChange={onChange} />
<Layout>
<SideBar>
<HorizontalGutter size="double">
<Navigation>
<Localized id="configure-sideBarNavigation-authentication">
<Link to="/admin/configure/auth">Auth</Link>
</Localized>
<Link to="/admin/configure/misc">Misc</Link>
</Navigation>
</HorizontalGutter>
<Localized id="configure-sideBar-saveChanges">
<Button
id="configure-sideBar-saveChanges"
color="success"
variant="filled"
type="submit"
disabled={submitting || pristine}
>
Save Changes
</Button>
</Localized>
</SideBar>
<Main>
{React.cloneElement(React.Children.only(children), {
form,
submitting,
})}
</Main>
</Layout>
</form>
)}
</Form>
</div>
);
export default Configure;
@@ -0,0 +1,5 @@
.root {
padding-bottom: calc(0.5 * var(--spacing-unit));
border-bottom: 1px solid var(--palette-text-primary);
margin-bottom: calc(1.5 * var(--spacing-unit));
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Header from "./Header";
it("renders correctly", () => {
const props: PropTypesOf<typeof Header> = {
children: "child",
};
const wrapper = shallow(<Header {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,12 @@
import React, { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import styles from "./Header.css";
const Header: StatelessComponent = ({ children }) => (
<Typography variant="heading1" className={styles.root}>
{children}
</Typography>
);
export default Header;
@@ -0,0 +1,5 @@
.root {
border: 0;
border-bottom: 1px solid var(--palette-divider);
padding-top: 1px;
}
@@ -0,0 +1 @@
export const root: string;
@@ -0,0 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import HorizontalRule from "./HorizontalRule";
it("renders correctly", () => {
const wrapper = shallow(<HorizontalRule />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,9 @@
import React, { StatelessComponent } from "react";
import styles from "./HorizontalRule.css";
const HorizontalRule: StatelessComponent = ({ children }) => (
<hr className={styles.root} />
);
export default HorizontalRule;
@@ -0,0 +1,4 @@
.root {
padding: calc(3 * var(--spacing-unit)) calc(5 * var(--spacing-unit))
calc(5 * var(--spacing-unit)) calc(5 * var(--spacing-unit));
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Layout from "./Layout";
it("renders correctly", () => {
const props: PropTypesOf<typeof Layout> = {
children: "child",
};
const wrapper = shallow(<Layout {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,10 @@
import React, { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import styles from "./Layout.css";
const Layout: StatelessComponent = ({ children }) => (
<Flex className={styles.root}>{children}</Flex>
);
export default Layout;
@@ -0,0 +1,4 @@
.root {
width: 100%;
max-width: calc(67 * var(--spacing-unit));
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Main from "./Main";
it("renders correctly", () => {
const props: PropTypesOf<typeof Main> = {
children: "child",
};
const wrapper = shallow(<Main {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,9 @@
import React, { StatelessComponent } from "react";
import styles from "./Main.css";
const Main: StatelessComponent = ({ children }) => (
<div className={styles.root}>{children}</div>
);
export default Main;
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Misc from "./Misc";
it("renders correctly", () => {
const props: PropTypesOf<typeof Misc> = {
children: "child",
};
const wrapper = shallow(<Misc {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,14 @@
import React, { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import Header from "./Header";
const Misc: StatelessComponent = ({ children }) => (
<div>
<Header>Misc Integrations</Header>
<Typography>Other stuff</Typography>
</div>
);
export default Misc;
@@ -0,0 +1,26 @@
.link {
display: inline-block;
text-decoration: none;
text-transform: uppercase;
padding: calc(0.5 * var(--spacing-unit)) var(--spacing-unit)
calc(0.5 * var(--spacing-unit)) calc(var(--spacing-unit) + 2px);
margin-left: 2px;
border-left: 1px solid var(--palette-grey-lighter);
color: var(--palette-text-primary);
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-bold);
font-size: calc(18rem / var(--rem-base));
line-height: calc(20em / 18);
letter-spacing: calc(-0.1em / 18);
&:hover {
cursor: pointer;
}
}
.linkActive {
margin-left: 0px;
border-left: calc(0.5 * var(--spacing-unit)) solid var(--palette-brand);
padding-left: var(--spacing-unit);
}
@@ -0,0 +1,16 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Link from "./Link";
it("renders correctly", () => {
const props: PropTypesOf<typeof Link> = {
className: "customClassName",
to: "/admin",
children: "child",
};
const wrapper = shallow(<Link {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,24 @@
import { Link as FoundLink, LocationDescriptor } from "found";
import React, { StatelessComponent } from "react";
import styles from "./Link.css";
interface Props {
className?: string;
children: React.ReactNode;
to: string | LocationDescriptor;
}
const Link: StatelessComponent<Props> = props => (
<li className={props.className}>
<FoundLink
to={props.to}
className={styles.link}
activeClassName={styles.linkActive}
>
{props.children}
</FoundLink>
</li>
);
export default Link;
@@ -0,0 +1,7 @@
.root {
}
.ul {
list-style: none;
padding: 0;
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Navigation from "./Navigation";
it("renders correctly", () => {
const props: PropTypesOf<typeof Navigation> = {
children: "child",
};
const wrapper = shallow(<Navigation {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,11 @@
import React, { StatelessComponent } from "react";
import styles from "./Navigation.css";
const Navigation: StatelessComponent = ({ children }) => (
<nav className={styles.root}>
<ul className={styles.ul}>{children}</ul>
</nav>
);
export default Navigation;
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<li
className="customClassName"
>
<Link
activeClassName="Link-linkActive"
className="Link-link"
to="/admin"
>
child
</Link>
</li>
`;
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<nav
className="Navigation-root"
>
<ul
className="Navigation-ul"
>
child
</ul>
</nav>
`;
@@ -0,0 +1,2 @@
export { default as Navigation } from "./Navigation";
export { default as Link } from "./Link";
@@ -0,0 +1,8 @@
.root {
width: calc(25 * var(--spacing-unit));
padding-top: calc(0.5 * var(--spacing-unit));
padding-right: calc(3 * var(--spacing-unit));
position: sticky;
top: 0;
align-self: flex-start;
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import SideBar from "./SideBar";
it("renders correctly", () => {
const props: PropTypesOf<typeof SideBar> = {
children: "child",
};
const wrapper = shallow(<SideBar {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,9 @@
import React, { StatelessComponent } from "react";
import styles from "./SideBar.css";
const SideBar: StatelessComponent = ({ children }) => (
<div className={styles.root}>{children}</div>
);
export default SideBar;
@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="ConfigBox-root"
>
<withPropsOnChange(Flex)
className="ConfigBox-title"
justifyContent="space-between"
>
<div>
<span>
title
</span>
</div>
<div>
<span>
topRight
</span>
</div>
</withPropsOnChange(Flex)>
<div
className="ConfigBox-content"
>
child
</div>
</div>
`;
@@ -1,11 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(HorizontalGutter)>
<withPropsOnChange(Typography)
variant="heading3"
<div
id="configure-container"
>
<ReactFinalForm
onSubmit={[Function]}
>
Configure
</withPropsOnChange(Typography)>
</withPropsOnChange(HorizontalGutter)>
<Component />
</ReactFinalForm>
</div>
`;
@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Typography)
className="Header-root"
variant="heading1"
>
child
</withPropsOnChange(Typography)>
`;
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<hr
className="HorizontalRule-root"
/>
`;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
className="Layout-root"
>
child
</withPropsOnChange(Flex)>
`;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="Main-root"
>
child
</div>
`;
@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div>
<Header>
Misc Integrations
</Header>
<withPropsOnChange(Typography)>
Other stuff
</withPropsOnChange(Typography)>
</div>
`;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="SideBar-root"
>
child
</div>
`;
@@ -0,0 +1,104 @@
import { FormApi, FormState } from "final-form";
import { Router } from "found";
import React from "react";
import {
UpdateSettingsInput,
UpdateSettingsMutation,
withUpdateSettingsMutation,
} from "talk-admin/mutations";
import { TalkContext, withContext } from "talk-framework/lib/bootstrap";
import { BadUserInputError } from "talk-framework/lib/errors";
import { getMessage } from "talk-framework/lib/i18n";
import Configure from "../components/Configure";
import {
AddSubmitHook,
SubmitHook,
SubmitHookContextProvider,
} from "../submitHook";
interface Props {
localeBundles: TalkContext["localeBundles"];
router: Router;
updateSettings: UpdateSettingsMutation;
children: React.ReactNode;
}
class ConfigureContainer extends React.Component<Props> {
private dirty = false;
private removeTransitionHook: () => void;
private submitHooks: SubmitHook[] = [];
constructor(props: Props) {
super(props);
this.dirty = false;
const warningMessage = getMessage(
props.localeBundles,
"configure-unsavedInputWarning",
"You have unsaved input. Are you sure you want to leave this page?"
);
this.removeTransitionHook = props.router.addTransitionHook(
() => (this.dirty ? warningMessage : true)
);
}
public componentWillUnmount() {
this.removeTransitionHook();
}
private handleSave = async (
data: UpdateSettingsInput["settings"],
form: FormApi
) => {
try {
// Call submit hooks, that can manipulate what
// we send as the mutation.
let nextData = data;
for (const hook of this.submitHooks) {
const result = await hook(nextData);
if (result) {
nextData = result;
}
}
await this.props.updateSettings({ settings: nextData });
form.initialize(data);
} catch (error) {
if (error instanceof BadUserInputError) {
return error.invalidArgsLocalized;
}
// tslint:disable-next-line:no-console
console.error(error);
}
return undefined;
};
private handleChange = ({ dirty }: FormState) => {
this.dirty = dirty;
};
private addSubmitHook: AddSubmitHook = hook => {
this.submitHooks.push(hook);
return () => {
this.submitHooks = this.submitHooks.filter(h => h === hook);
};
};
public render() {
return (
<SubmitHookContextProvider value={this.addSubmitHook}>
<Configure onChange={this.handleChange} onSave={this.handleSave}>
{this.props.children}
</Configure>
</SubmitHookContextProvider>
);
}
}
const enhanced = withContext(({ localeBundles }) => ({ localeBundles }))(
withUpdateSettingsMutation(ConfigureContainer)
);
export default enhanced;
@@ -0,0 +1,31 @@
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { HorizontalGutter } from "talk-ui/components";
import DisplayNamesConfigContainer from "../containers/DisplayNamesConfigContainer";
import AuthIntegrationsConfig from "./AuthIntegrationsConfig";
interface Props {
disabled?: boolean;
auth: PropTypesOf<typeof AuthIntegrationsConfig>["auth"] &
PropTypesOf<typeof DisplayNamesConfigContainer>["auth"];
onInitValues: (values: any) => void;
}
const Auth: StatelessComponent<Props> = ({ disabled, auth, onInitValues }) => (
<HorizontalGutter size="double">
<DisplayNamesConfigContainer
disabled={disabled}
auth={auth}
onInitValues={onInitValues}
/>
<AuthIntegrationsConfig
disabled={disabled}
auth={auth}
onInitValues={onInitValues}
/>
</HorizontalGutter>
);
export default Auth;
@@ -0,0 +1,69 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { HorizontalGutter } from "talk-ui/components";
import Header from "../../../components/Header";
import FacebookConfigContainer from "../containers/FacebookConfigContainer";
import GoogleConfigContainer from "../containers/GoogleConfigContainer";
import LocalAuthConfigContainer from "../containers/LocalAuthConfigContainer";
import OIDCConfigListContainer from "../containers/OIDCConfigListContainer";
import SSOConfigContainer from "../containers/SSOConfigContainer";
interface Props {
disabled?: boolean;
auth: PropTypesOf<typeof FacebookConfigContainer>["auth"] &
PropTypesOf<typeof FacebookConfigContainer>["authReadOnly"] &
PropTypesOf<typeof GoogleConfigContainer>["auth"] &
PropTypesOf<typeof GoogleConfigContainer>["authReadOnly"] &
PropTypesOf<typeof SSOConfigContainer>["auth"] &
PropTypesOf<typeof SSOConfigContainer>["authReadOnly"] &
PropTypesOf<typeof LocalAuthConfigContainer>["auth"] &
PropTypesOf<typeof OIDCConfigListContainer>["auth"] &
PropTypesOf<typeof OIDCConfigListContainer>["authReadOnly"];
onInitValues: (values: any) => void;
}
const AuthIntegrationsConfig: StatelessComponent<Props> = ({
disabled,
auth,
onInitValues,
}) => (
<HorizontalGutter size="double">
<Localized id="configure-auth-authIntegrations">
<Header>Auth Integrations</Header>
</Localized>
<LocalAuthConfigContainer
disabled={disabled}
auth={auth}
onInitValues={onInitValues}
/>
<OIDCConfigListContainer
disabled={disabled}
auth={auth}
authReadOnly={auth}
onInitValues={onInitValues}
/>
<SSOConfigContainer
disabled={disabled}
auth={auth}
authReadOnly={auth}
onInitValues={onInitValues}
/>
<GoogleConfigContainer
disabled={disabled}
auth={auth}
authReadOnly={auth}
onInitValues={onInitValues}
/>
<FacebookConfigContainer
disabled={disabled}
auth={auth}
authReadOnly={auth}
onInitValues={onInitValues}
/>
</HorizontalGutter>
);
export default AuthIntegrationsConfig;
@@ -0,0 +1,51 @@
import { Localized } from "fluent-react/compat";
import { identity } from "lodash";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { Validator } from "talk-framework/lib/validation";
import { FormField, InputLabel, TextField } from "talk-ui/components";
import ValidationMessage from "./ValidationMessage";
interface Props {
validate?: Validator;
name: string;
disabled: boolean;
}
const ClientSecretField: StatelessComponent<Props> = ({
name,
disabled,
validate,
}) => (
<FormField>
<Localized id="configure-auth-clientID">
<InputLabel>Client ID</InputLabel>
</Localized>
<Field name={name} parse={identity} validate={validate}>
{({ input, meta }) => (
<>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
);
export default ClientSecretField;
@@ -0,0 +1,56 @@
import { Localized } from "fluent-react/compat";
import { identity } from "lodash";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { Validator } from "talk-framework/lib/validation";
import { FormField, InputLabel, TextField } from "talk-ui/components";
import ValidationMessage from "./ValidationMessage";
interface Props {
validate?: Validator;
name: string;
disabled: boolean;
}
const ClientSecretField: StatelessComponent<Props> = ({
name,
disabled,
validate,
}) => (
<FormField>
<Localized id="configure-auth-clientSecret">
<InputLabel>Client Secret</InputLabel>
</Localized>
<Field
name={name}
key={(disabled && "on") || "off"}
parse={identity}
validate={validate}
>
{({ input, meta }) => (
<>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
);
export default ClientSecretField;
@@ -0,0 +1,54 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { CheckBox, FormField } from "talk-ui/components";
import ConfigBox from "../../../components/ConfigBox";
interface Props {
id?: string;
name: string;
title: React.ReactNode;
disabled?: boolean;
children: (disabledInside: boolean) => React.ReactNode;
}
const bool = (v: any) => !!v;
const ConfigBoxWithToggleField: StatelessComponent<Props> = ({
id,
name,
title,
disabled,
children,
}) => (
<Field name={name} type="checkbox" parse={bool}>
{({ input }) => (
<ConfigBox
id={id}
title={title}
topRight={
<FormField>
<Localized id="configure-auth-configBoxEnabled">
<CheckBox
id={input.name}
name={input.name}
onChange={input.onChange}
checked={input.value}
disabled={disabled}
light
>
Enabled
</CheckBox>
</Localized>
</FormField>
}
>
{children(disabled || !input.value)}
</ConfigBox>
)}
</Field>
);
export default ConfigBoxWithToggleField;
@@ -0,0 +1,3 @@
.description {
width: calc(30 * var(--spacing-unit));
}
@@ -0,0 +1,22 @@
import React, { StatelessComponent } from "react";
import { InputDescription } from "talk-ui/components";
import { PropTypesOf } from "talk-ui/types";
interface Props {
container?: PropTypesOf<typeof InputDescription>["container"];
children: React.ReactNode;
}
import styles from "./ConfigDescription.css";
const ConfigDescription: StatelessComponent<Props> = ({
children,
container,
}) => (
<InputDescription className={styles.description} container={container}>
{children}
</InputDescription>
);
export default ConfigDescription;
@@ -0,0 +1,94 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import {
Flex,
FormField,
HorizontalGutter,
RadioButton,
Typography,
} from "talk-ui/components";
import Header from "../../../components/Header";
interface Props {
disabled?: boolean;
}
const parseStringBool = (v: string) => v === "true";
const DisplayNamesConfig: StatelessComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf">
<Localized id="configure-auth-displayNamesConfig-title">
<Header>Display Names</Header>
</Localized>
<Localized id="configure-auth-displayNamesConfig-explanationShort">
<Typography>
Some AUTH integrations include a Display Name as well as a User Name.
</Typography>
</Localized>
<Localized id="configure-auth-displayNamesConfig-explanationLong">
<Typography>
A User Name has to be unique (there can only be one Juan_Doe, for
example), whereas a Display Name does not. If your AUTH provider allows
for Display Names, you can enable this option. This allows for fewer
strange names (Juan_Doe23245) however it could also be used to
spoof/impersonate another user.
</Typography>
</Localized>
<FormField>
<Flex direction="row" itemGutter="double">
<Field
name={"auth.displayName.enabled"}
type="radio"
parse={parseStringBool}
value
>
{({ input }) => (
<Localized id="configure-auth-displayNamesConfig-showDisplayNames">
<RadioButton
id={`${input.name}-true`}
name={input.name}
onChange={input.onChange}
onFocus={input.onFocus}
onBlur={input.onBlur}
checked={input.checked}
disabled={disabled}
value={input.value}
>
Show Display Names (if available)
</RadioButton>
</Localized>
)}
</Field>
<Field
name={"auth.displayName.enabled"}
type="radio"
parse={parseStringBool}
value={false}
>
{({ input }) => (
<Localized id="configure-auth-displayNamesConfig-hideDisplayNames">
<RadioButton
id={`${input.name}-false`}
name={input.name}
onChange={input.onChange}
onFocus={input.onFocus}
onBlur={input.onBlur}
checked={input.checked}
disabled={disabled}
value={input.value}
>
Hide Display Names (if available)
</RadioButton>
</Localized>
)}
</Field>
</Flex>
</FormField>
</HorizontalGutter>
);
export default DisplayNamesConfig;
@@ -0,0 +1,93 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { required, Validator } from "talk-framework/lib/validation";
import { HorizontalGutter, TextLink, Typography } from "talk-ui/components";
import HorizontalRule from "../../../components/HorizontalRule";
import ClientIDField from "./ClientIDField";
import ClientSecretField from "./ClientSecretField";
import ConfigBoxWithToggleField from "./ConfigBoxWithToggleField";
import RedirectField from "./RedirectField";
import RegistrationField from "./RegistrationField";
import TargetFilterField from "./TargetFilterField";
interface Props {
disabled?: boolean;
callbackURL: string;
}
const FacebookLink = () => (
<TextLink target="_blank">
{"https://developers.facebook.com/docs/facebook-login/web"}
</TextLink>
);
const validateWhenEnabled = (validator: Validator): Validator => (
v,
values
) => {
if (values.auth.integrations.facebook.enabled) {
return validator(v, values);
}
return "";
};
const FacebookConfig: StatelessComponent<Props> = ({
disabled,
callbackURL,
}) => (
<ConfigBoxWithToggleField
id="configure-auth-facebook-container"
title={
<Localized id="configure-auth-facebook-loginWith">
<span>Login with Facebook</span>
</Localized>
}
name="auth.integrations.facebook.enabled"
disabled={disabled}
>
{disabledInside => (
<HorizontalGutter size="double">
<Localized
id="configure-auth-facebook-toEnableIntegration"
link={<FacebookLink />}
>
<Typography>
To enable the integration with Facebook Authentication, you need to
create and set up a web application. For more information visit:<br />
{"https://developers.facebook.com/docs/facebook-login/web"}
</Typography>
</Localized>
<HorizontalRule />
<RedirectField url={callbackURL} />
<HorizontalRule />
<ClientIDField
name="auth.integrations.facebook.clientID"
validate={validateWhenEnabled(required)}
disabled={disabledInside}
/>
<ClientSecretField
name="auth.integrations.facebook.clientSecret"
validate={validateWhenEnabled(required)}
disabled={disabledInside}
/>
<TargetFilterField
label={
<Localized id="configure-auth-facebook-useLoginOn">
<span>Use Facebook login on</span>
</Localized>
}
name="auth.integrations.facebook.targetFilter"
disabled={disabledInside}
/>
<RegistrationField
name="auth.integrations.facebook.allowRegistration"
disabled={disabledInside}
/>
</HorizontalGutter>
)}
</ConfigBoxWithToggleField>
);
export default FacebookConfig;
@@ -0,0 +1,93 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { required, Validator } from "talk-framework/lib/validation";
import { HorizontalGutter, TextLink, Typography } from "talk-ui/components";
import HorizontalRule from "../../../components/HorizontalRule";
import ClientIDField from "./ClientIDField";
import ClientSecretField from "./ClientSecretField";
import ConfigBoxWithToggleField from "./ConfigBoxWithToggleField";
import RedirectField from "./RedirectField";
import RegistrationField from "./RegistrationField";
import TargetFilterField from "./TargetFilterField";
interface Props {
disabled?: boolean;
callbackURL: string;
}
const GoogleLink = () => (
<TextLink target="_blank">
{
"https://developers.google.com/identity/protocols/OAuth2WebServer#creatingcred"
}
</TextLink>
);
const validateWhenEnabled = (validator: Validator): Validator => (
v,
values
) => {
if (values.auth.integrations.google.enabled) {
return validator(v, values);
}
return "";
};
const GoogleConfig: StatelessComponent<Props> = ({ disabled, callbackURL }) => (
<ConfigBoxWithToggleField
title={
<Localized id="configure-auth-google-loginWith">
<span>Login with Google</span>
</Localized>
}
name="auth.integrations.google.enabled"
disabled={disabled}
>
{disabledInside => (
<HorizontalGutter size="double">
<Localized
id="configure-auth-google-toEnableIntegration"
link={<GoogleLink />}
>
<Typography>
To enable the integration with Google Authentication you need to
create and set up a web application. For more information visit:<br />
{
"https://developers.google.com/identity/protocols/OAuth2WebServer#creatingcred"
}
</Typography>
</Localized>
<HorizontalRule />
<RedirectField url={callbackURL} />
<HorizontalRule />
<ClientIDField
name="auth.integrations.google.clientID"
validate={validateWhenEnabled(required)}
disabled={disabledInside}
/>
<ClientSecretField
name="auth.integrations.google.clientSecret"
validate={validateWhenEnabled(required)}
disabled={disabledInside}
/>
<TargetFilterField
label={
<Localized id="configure-auth-google-useLoginOn">
<span>Use Google login on</span>
</Localized>
}
name="auth.integrations.google.targetFilter"
disabled={disabledInside}
/>
<RegistrationField
name="auth.integrations.google.allowRegistration"
disabled={disabledInside}
/>
</HorizontalGutter>
)}
</ConfigBoxWithToggleField>
);
export default GoogleConfig;
@@ -0,0 +1,44 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { HorizontalGutter } from "talk-ui/components";
import ConfigBoxWithToggleField from "./ConfigBoxWithToggleField";
import RegistrationField from "./RegistrationField";
import TargetFilterField from "./TargetFilterField";
interface Props {
disabled?: boolean;
}
const LocalAuthConfig: StatelessComponent<Props> = ({ disabled }) => (
<ConfigBoxWithToggleField
title={
<Localized id="configure-auth-local-loginWith">
<span>Login with LocalAuth</span>
</Localized>
}
name="auth.integrations.local.enabled"
disabled={disabled}
>
{disabledInside => (
<HorizontalGutter size="double">
<TargetFilterField
label={
<Localized id="configure-auth-local-useLoginOn">
<span>Use LocalAuth login on</span>
</Localized>
}
name="auth.integrations.local.targetFilter"
disabled={disabledInside}
/>
<RegistrationField
name="auth.integrations.local.allowRegistration"
disabled={disabledInside}
/>
</HorizontalGutter>
)}
</ConfigBoxWithToggleField>
);
export default LocalAuthConfig;
@@ -0,0 +1,4 @@
.redirectDescriptionIcon {
color: var(--palette-text-secondary);
flex-shrink: 0;
}
@@ -0,0 +1,317 @@
import { Localized } from "fluent-react/compat";
import { identity } from "lodash";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import {
composeValidators,
required,
validateURL,
Validator,
} from "talk-framework/lib/validation";
import {
Button,
Flex,
FormField,
HorizontalGutter,
Icon,
InputLabel,
TextField,
TextLink,
Typography,
} from "talk-ui/components";
import HorizontalRule from "../../../components/HorizontalRule";
import ClientIDField from "./ClientIDField";
import ClientSecretField from "./ClientSecretField";
import ConfigBoxWithToggleField from "./ConfigBoxWithToggleField";
import ConfigDescription from "./ConfigDescription";
import styles from "./OIDCConfig.css";
import RedirectField from "./RedirectField";
import RegistrationField from "./RegistrationField";
import TargetFilterField from "./TargetFilterField";
import ValidationMessage from "./ValidationMessage";
interface Props {
index: number;
disabled?: boolean;
callbackURL: string;
disableForDiscover?: boolean;
onDiscover?: () => void;
}
const OIDCLink = () => (
<TextLink target="_blank">{"https://openid.net/connect/"}</TextLink>
);
const OIDCConfig: StatelessComponent<Props> = ({
disabled,
callbackURL,
index,
onDiscover,
disableForDiscover,
}) => {
const validateWhenEnabled = (validator: Validator): Validator => (
v,
values
) => {
if (values.auth.integrations.oidc[0].enabled) {
return validator(v, values);
}
return "";
};
return (
<ConfigBoxWithToggleField
id={`configure-auth-oidc-container-${index}`}
title={
<Localized id="configure-auth-oidc-loginWith">
<span>Login with OIDC</span>
</Localized>
}
name={`auth.integrations.oidc.${index}.enabled`}
disabled={disabled}
>
{disabledInside => (
<HorizontalGutter size="double">
<Localized id="configure-auth-oidc-toLearnMore" link={<OIDCLink />}>
<Typography>
{"To learn more: https://openid.net/connect/"}
</Typography>
</Localized>
<HorizontalRule />
<RedirectField
url={callbackURL}
description={
<ConfigDescription container="div">
<Flex itemGutter="half">
<Icon className={styles.redirectDescriptionIcon}>error</Icon>
<Localized id="configure-auth-oidc-redirectDescription">
<div>
For OpenID Connect, your Redirect URI will not appear
until you after you save this integration
</div>
</Localized>
</Flex>
</ConfigDescription>
}
/>
<HorizontalRule />
<FormField>
<Localized id="configure-auth-oidc-providerName">
<InputLabel>Provider Name</InputLabel>
</Localized>
<Localized id="configure-auth-oidc-providerNameDescription">
<ConfigDescription>
The provider of the OIDC integration. This will be used when the
name of the provider needs to be displayed, e.g. Log in with
{" <Facebook>"}
</ConfigDescription>
</Localized>
<Field
name={`auth.integrations.oidc.${index}.name`}
validate={validateWhenEnabled(required)}
parse={identity}
>
{({ input, meta }) => (
<>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabledInside}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
<ClientIDField
validate={validateWhenEnabled(required)}
name={`auth.integrations.oidc.${index}.clientID`}
disabled={disabledInside}
/>
<ClientSecretField
validate={validateWhenEnabled(required)}
name={`auth.integrations.oidc.${index}.clientSecret`}
disabled={disabledInside}
/>
<FormField>
<Localized id="configure-auth-oidc-issuer">
<InputLabel>Issuer</InputLabel>
</Localized>
<Localized id="configure-auth-oidc-issuerDescription">
<ConfigDescription>
After entering your Issuer information, click the Discover
button to have Talk complete the remaining fields. You may also
enter the information manually
</ConfigDescription>
</Localized>
<Field
name={`auth.integrations.oidc.${index}.issuer`}
validate={validateWhenEnabled(
composeValidators(required, validateURL)
)}
parse={identity}
>
{({ input, meta }) => (
<>
<Flex direction="row" itemGutter="half" alignItems="center">
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabledInside || disableForDiscover}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
/>
<Button
id="configure-auth-oidc-discover-0"
variant="filled"
color="primary"
size="small"
disabled={disabledInside || disableForDiscover}
onClick={onDiscover}
>
Discover
</Button>
</Flex>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
<FormField>
<Localized id="configure-auth-oidc-authorizationURL">
<InputLabel>authorizationURL</InputLabel>
</Localized>
<Field
name={`auth.integrations.oidc.${index}.authorizationURL`}
validate={validateWhenEnabled(
composeValidators(required, validateURL)
)}
parse={identity}
>
{({ input, meta }) => (
<>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabledInside || disableForDiscover}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
<FormField>
<Localized id="configure-auth-oidc-tokenURL">
<InputLabel>tokenURL</InputLabel>
</Localized>
<Field
name={`auth.integrations.oidc.${index}.tokenURL`}
validate={validateWhenEnabled(
composeValidators(required, validateURL)
)}
parse={identity}
>
{({ input, meta }) => (
<>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabledInside || disableForDiscover}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
<FormField>
<Localized id="configure-auth-oidc-jwksURI">
<InputLabel>jwksURI</InputLabel>
</Localized>
<Field
name={`auth.integrations.oidc.${index}.jwksURI`}
validate={validateWhenEnabled(
composeValidators(required, validateURL)
)}
parse={identity}
>
{({ input, meta }) => (
<>
<TextField
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabledInside || disableForDiscover}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
<TargetFilterField
label={
<Localized id="configure-auth-oidc-useLoginOn">
<span>Use OIDC login on</span>
</Localized>
}
name={`auth.integrations.oidc.${index}.targetFilter`}
disabled={disabledInside}
/>
<RegistrationField
name={`auth.integrations.oidc.${index}.allowRegistration`}
disabled={disabledInside}
/>
</HorizontalGutter>
)}
</ConfigBoxWithToggleField>
);
};
export default OIDCConfig;
@@ -0,0 +1,25 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { CopyButton } from "talk-framework/components";
import { Flex, FormField, InputLabel, TextField } from "talk-ui/components";
interface Props {
description?: React.ReactNode;
url: string;
}
const RedirectField: StatelessComponent<Props> = ({ url, description }) => (
<FormField>
<Localized id="configure-auth-redirectURI">
<InputLabel>Redirect URI</InputLabel>
</Localized>
{description}
<Flex direction="row" itemGutter="half" alignItems="center">
<TextField name="redirectURI" value={url} readOnly />
<CopyButton text={url} />
</Flex>
</FormField>
);
export default RedirectField;
@@ -0,0 +1,44 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { CheckBox, FormField, InputLabel } from "talk-ui/components";
import ConfigDescription from "./ConfigDescription";
interface Props {
name: string;
disabled: boolean;
}
const RegistrationField: StatelessComponent<Props> = ({ name, disabled }) => (
<FormField>
<Localized id="configure-auth-registration">
<InputLabel>Registration</InputLabel>
</Localized>
<Localized id="configure-auth-registrationDescription">
<ConfigDescription>
Allow users to create a new account with this provider.
</ConfigDescription>
</Localized>
<FormField>
<Field name={name} type="checkbox">
{({ input }) => (
<Localized id="configure-auth-registrationCheckBox">
<CheckBox
id={input.name}
name={input.name}
onChange={input.onChange}
checked={input.value}
disabled={disabled}
>
Allow Registration
</CheckBox>
</Localized>
)}
</Field>
</FormField>
</FormField>
);
export default RegistrationField;
@@ -0,0 +1,48 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { HorizontalGutter } from "talk-ui/components";
import SSOKeyFieldContainer from "../containers/SSOKeyFieldContainer";
import ConfigBoxWithToggleField from "./ConfigBoxWithToggleField";
import RegistrationField from "./RegistrationField";
import TargetFilterField from "./TargetFilterField";
interface Props {
disabled?: boolean;
sso: PropTypesOf<typeof SSOKeyFieldContainer>["sso"];
}
const SSOConfig: StatelessComponent<Props> = ({ disabled, sso }) => (
<ConfigBoxWithToggleField
title={
<Localized id="configure-auth-sso-loginWith">
<span>Login with SSO</span>
</Localized>
}
name="auth.integrations.sso.enabled"
disabled={disabled}
>
{disabledInside => (
<HorizontalGutter size="double">
<SSOKeyFieldContainer sso={sso} disabled={disabledInside} />
<TargetFilterField
label={
<Localized id="configure-auth-sso-useLoginOn">
<span>Use SSO login on</span>
</Localized>
}
name="auth.integrations.sso.targetFilter"
disabled={disabledInside}
/>
<RegistrationField
name="auth.integrations.sso.allowRegistration"
disabled={disabledInside}
/>
</HorizontalGutter>
)}
</ConfigBoxWithToggleField>
);
export default SSOConfig;
@@ -0,0 +1,16 @@
.keyGenerated {
composes: button from "talk-ui/shared/typography.css";
color: var(--palette-text-secondary);
width: calc(29 * var(--spacing-unit));
flex-shrink: 0;
}
.warnIcon {
color: var(--palette-text-secondary);
flex-shrink: 0;
padding-top: 3px;
}
.warn {
color: var(--palette-text-secondary);
}
@@ -0,0 +1,65 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import {
Button,
Flex,
FormField,
Icon,
InputLabel,
TextField,
Typography,
} from "talk-ui/components";
import styles from "./SSOKeyField.css";
interface Props {
disabled?: boolean;
generatedKey?: string;
keyGeneratedAt?: any;
onRegenerate?: () => void;
}
const SSOKeyField: StatelessComponent<Props> = ({
generatedKey,
keyGeneratedAt,
disabled,
onRegenerate,
}) => (
<FormField id="configure-auth-sso-key">
<Localized id="configure-auth-sso-key">
<InputLabel>Key</InputLabel>
</Localized>
<Flex direction="row" itemGutter="half" alignItems="center">
<TextField name="key" value={generatedKey} readOnly />
<Localized id="configure-auth-sso-regenerate">
<Button
id="configure-auth-sso-regenerate"
variant="filled"
color="primary"
size="small"
disabled={disabled}
onClick={onRegenerate}
>
Regenerate
</Button>
</Localized>
</Flex>
<Flex direction="row" itemGutter="half">
<Localized id="configure-auth-sso-regenerateAt" $date={keyGeneratedAt}>
<Typography className={styles.keyGenerated}>
KEY GENERATED AT: {keyGeneratedAt}
</Typography>
</Localized>
<Icon className={styles.warnIcon}>warning</Icon>
<Localized id="configure-auth-sso-regenerateWarning">
<Typography className={styles.warn} variant="bodyCopy">
Regenerating a key will invalidate any existing user sessions, and all
signed-in users will be signed out
</Typography>
</Localized>
</Flex>
</FormField>
);
export default SSOKeyField;
@@ -0,0 +1,57 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { CheckBox, Flex, FormField, InputLabel } from "talk-ui/components";
const bool = (v: any) => !!v;
interface Props {
label: React.ReactNode;
name: string;
disabled: boolean;
}
const TargetFilterField: StatelessComponent<Props> = ({
name,
label,
disabled,
}) => (
<FormField>
<InputLabel>{label}</InputLabel>
<Flex direction="row" itemGutter="double">
<Field name={`${name}.admin`} type="checkbox" parse={bool}>
{({ input, meta }) => (
<Localized id="configure-auth-targetFilterTalkAdmin">
<CheckBox
id={input.name}
name={input.name}
onChange={input.onChange}
checked={!!input.value}
disabled={disabled}
>
Talk Admin
</CheckBox>
</Localized>
)}
</Field>
<Field name={`${name}.stream`} type="checkbox" parse={bool}>
{({ input }) => (
<Localized id="configure-auth-targetFilterCommentStream">
<CheckBox
id={input.name}
name={input.name}
onChange={input.onChange}
checked={!!input.value}
disabled={disabled}
>
Comment Stream
</CheckBox>
</Localized>
)}
</Field>
</Flex>
</FormField>
);
export default TargetFilterField;
@@ -0,0 +1,3 @@
.root {
min-width: calc(29 * var(--spacing-unit));
}
@@ -0,0 +1,15 @@
import React, { StatelessComponent } from "react";
import { ValidationMessage as UIValidationMessage } from "talk-ui/components";
interface Props {
children: React.ReactNode;
}
import styles from "./ValidationMessage.css";
const ValidationMessage: StatelessComponent<Props> = ({ children }) => (
<UIValidationMessage className={styles.root}>{children}</UIValidationMessage>
);
export default ValidationMessage;
@@ -0,0 +1,67 @@
import { FormApi } from "final-form";
import { RouteProps } from "found";
import { merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { AuthContainerQueryResponse } from "talk-admin/__generated__/AuthContainerQuery.graphql";
import { Spinner } from "talk-ui/components";
import Auth from "../components/Auth";
interface Props extends AuthContainerQueryResponse {
form: FormApi;
submitting?: boolean;
}
export default class AuthContainer extends React.Component<Props> {
public static routeConfig: RouteProps;
private initialValues = {};
constructor(props: Props) {
super(props);
}
public componentDidMount() {
this.props.form.initialize({ auth: this.initialValues });
}
private handleOnInitValues = (values: any) => {
this.initialValues = merge(this.initialValues, values);
};
public render() {
return (
<Auth
disabled={this.props.submitting}
auth={this.props.settings.auth}
onInitValues={this.handleOnInitValues}
/>
);
}
}
AuthContainer.routeConfig = {
Component: AuthContainer,
query: graphql`
query AuthContainerQuery {
settings {
auth {
...FacebookConfigContainer_auth
...FacebookConfigContainer_authReadOnly
...GoogleConfigContainer_auth
...GoogleConfigContainer_authReadOnly
...SSOConfigContainer_auth
...SSOConfigContainer_authReadOnly
...LocalAuthConfigContainer_auth
...DisplayNamesConfigContainer_auth
...OIDCConfigListContainer_auth
...OIDCConfigListContainer_authReadOnly
}
}
}
`,
cacheConfig: { force: true },
render: ({ Component, props }) =>
props && Component ? <Component {...props} /> : <Spinner />,
};
@@ -0,0 +1,37 @@
import React from "react";
import { graphql } from "react-relay";
import { DisplayNamesConfigContainer_auth as AuthData } from "talk-admin/__generated__/DisplayNamesConfigContainer_auth.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import DisplayNamesConfig from "../components/DisplayNamesConfig";
interface Props {
auth: AuthData;
onInitValues: (values: AuthData) => void;
disabled?: boolean;
}
class DisplayNamesConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.auth);
}
public render() {
const { disabled } = this.props;
return <DisplayNamesConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
auth: graphql`
fragment DisplayNamesConfigContainer_auth on Auth {
displayName {
enabled
}
}
`,
})(DisplayNamesConfigContainer);
export default enhanced;
@@ -0,0 +1,62 @@
import React from "react";
import { graphql } from "react-relay";
import { FacebookConfigContainer_auth as AuthData } from "talk-admin/__generated__/FacebookConfigContainer_auth.graphql";
import { FacebookConfigContainer_authReadOnly as AuthReadOnlyData } from "talk-admin/__generated__/FacebookConfigContainer_authReadOnly.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import FacebookConfig from "../components/FacebookConfig";
interface Props {
auth: AuthData;
authReadOnly: AuthReadOnlyData;
onInitValues: (values: AuthData) => void;
disabled?: boolean;
}
class FacebookConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.auth);
}
public render() {
const { disabled, authReadOnly } = this.props;
return (
<FacebookConfig
disabled={disabled}
callbackURL={authReadOnly.integrations.facebook.callbackURL}
/>
);
}
}
const enhanced = withFragmentContainer<Props>({
auth: graphql`
fragment FacebookConfigContainer_auth on Auth {
integrations {
facebook {
enabled
allowRegistration
targetFilter {
admin
stream
}
clientID
clientSecret
}
}
}
`,
authReadOnly: graphql`
fragment FacebookConfigContainer_authReadOnly on Auth {
integrations {
facebook {
callbackURL
}
}
}
`,
})(FacebookConfigContainer);
export default enhanced;
@@ -0,0 +1,62 @@
import React from "react";
import { graphql } from "react-relay";
import { GoogleConfigContainer_auth as AuthData } from "talk-admin/__generated__/GoogleConfigContainer_auth.graphql";
import { GoogleConfigContainer_authReadOnly as AuthReadOnlyData } from "talk-admin/__generated__/GoogleConfigContainer_authReadOnly.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import GoogleConfig from "../components/GoogleConfig";
interface Props {
auth: AuthData;
authReadOnly: AuthReadOnlyData;
onInitValues: (values: AuthData) => void;
disabled?: boolean;
}
class GoogleConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.auth);
}
public render() {
const { disabled, authReadOnly } = this.props;
return (
<GoogleConfig
disabled={disabled}
callbackURL={authReadOnly.integrations.google.callbackURL}
/>
);
}
}
const enhanced = withFragmentContainer<Props>({
auth: graphql`
fragment GoogleConfigContainer_auth on Auth {
integrations {
google {
enabled
allowRegistration
targetFilter {
admin
stream
}
clientID
clientSecret
}
}
}
`,
authReadOnly: graphql`
fragment GoogleConfigContainer_authReadOnly on Auth {
integrations {
google {
callbackURL
}
}
}
`,
})(GoogleConfigContainer);
export default enhanced;
@@ -0,0 +1,44 @@
import React from "react";
import { graphql } from "react-relay";
import { LocalAuthConfigContainer_auth as AuthData } from "talk-admin/__generated__/LocalAuthConfigContainer_auth.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import LocalAuthConfig from "../components/LocalAuthConfig";
interface Props {
auth: AuthData;
onInitValues: (values: AuthData) => void;
disabled?: boolean;
}
class LocalAuthConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.auth);
}
public render() {
const { disabled } = this.props;
return <LocalAuthConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
auth: graphql`
fragment LocalAuthConfigContainer_auth on Auth {
integrations {
local {
enabled
allowRegistration
targetFilter {
admin
stream
}
}
}
}
`,
})(LocalAuthConfigContainer);
export default enhanced;
@@ -0,0 +1,70 @@
import { FormApi } from "final-form";
import PropTypes from "prop-types";
import React from "react";
import {
DiscoverOIDCConfigurationFetch,
withDiscoverOIDCConfigurationFetch,
} from "talk-admin/fetches";
import OIDCConfig from "../components/OIDCConfig";
interface Props {
index: number;
callbackURL: string;
disabled?: boolean;
discoverOIDCConfiguration: DiscoverOIDCConfigurationFetch;
}
interface State {
awaitingResponse: boolean;
}
class OIDCConfigContainer extends React.Component<Props, State> {
public static contextTypes = {
reactFinalForm: PropTypes.object,
};
public state = {
awaitingResponse: false,
};
private handleDiscover = async () => {
const form = this.context.reactFinalForm as FormApi;
this.setState({ awaitingResponse: true });
try {
const config = await this.props.discoverOIDCConfiguration({
issuer: form.getState().values.auth.integrations.oidc[0].issuer,
});
if (config) {
form.change(
"auth.integrations.oidc.0.authorizationURL",
config.authorizationURL
);
form.change("auth.integrations.oidc.0.jwksURI", config.jwksURI);
form.change("auth.integrations.oidc.0.tokenURL", config.tokenURL);
}
} catch (error) {
// tslint:disable-next-line:no-console
console.warn(error);
}
this.setState({ awaitingResponse: false });
};
public render() {
const { disabled, index, callbackURL } = this.props;
return (
<OIDCConfig
disabled={disabled}
index={index}
callbackURL={callbackURL}
onDiscover={this.handleDiscover}
disableForDiscover={this.state.awaitingResponse}
/>
);
}
}
const enhanced = withDiscoverOIDCConfigurationFetch(OIDCConfigContainer);
export default enhanced;
@@ -0,0 +1,150 @@
import { cloneDeep } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { OIDCConfigListContainer_auth as AuthData } from "talk-admin/__generated__/OIDCConfigListContainer_auth.graphql";
import { OIDCConfigListContainer_authReadOnly as AuthReadOnlyData } from "talk-admin/__generated__/OIDCConfigListContainer_authReadOnly.graphql";
import {
CreateOIDCAuthIntegrationMutation,
UpdateOIDCAuthIntegrationMutation,
withCreateOIDCAuthIntegrationMutation,
withUpdateOIDCAuthIntegrationMutation,
} from "talk-admin/mutations";
import { withFragmentContainer } from "talk-framework/lib/relay";
import {
AddSubmitHook,
RemoveSubmitHook,
SubmitHook,
withSubmitHookContext,
} from "../../../submitHook";
import OIDCConfigContainer from "./OIDCConfigContainer";
interface Props {
auth: AuthData;
authReadOnly: AuthReadOnlyData;
disabled?: boolean;
addSubmitHook: AddSubmitHook;
onInitValues: (values: AuthData) => void;
createOIDCAuthIntegration: CreateOIDCAuthIntegrationMutation;
updateOIDCAuthIntegration: UpdateOIDCAuthIntegrationMutation;
}
class OIDCConfigListContainer extends React.Component<Props> {
private removeSubmitHook: RemoveSubmitHook;
constructor(props: Props) {
super(props);
props.onInitValues(this.getAuthWithDefault());
this.removeSubmitHook = this.props.addSubmitHook(this.submitHook);
}
public componentWillUnmount() {
this.removeSubmitHook();
}
private submitHook: SubmitHook = async (data: any) => {
const cloned = cloneDeep(data);
const oidc = cloned.auth.integrations.oidc;
delete cloned.auth.integrations.oidc;
if (this.props.auth.integrations.oidc.length === 0) {
if (oidc[0].enabled) {
await this.props.createOIDCAuthIntegration({ configuration: oidc[0] });
}
} else {
await this.props.updateOIDCAuthIntegration({
configuration: oidc[0],
id: this.props.authReadOnly.integrations.oidc[0].id,
});
}
return cloned;
};
private getAuthWithDefault(): AuthData {
return this.props.auth.integrations.oidc.length === 0
? ({
integrations: {
oidc: [
{
clientID: "",
clientSecret: "",
allowRegistration: false,
targetFilter: {
admin: true,
stream: true,
},
name: "",
authorizationURL: "",
tokenURL: "",
jwksURI: "",
issuer: "",
},
],
},
} as any)
: this.props.auth;
}
public render() {
const { disabled, authReadOnly } = this.props;
const integrations = this.getAuthWithDefault().integrations.oidc.map(
(data, i) => (
<OIDCConfigContainer
key={i}
disabled={disabled}
index={i}
callbackURL={
(authReadOnly.integrations.oidc[i] &&
authReadOnly.integrations.oidc[i].callbackURL) ||
""
}
/>
)
);
return <>{integrations}</>;
}
}
const enhanced = withFragmentContainer<Props>({
auth: graphql`
fragment OIDCConfigListContainer_auth on Auth {
integrations {
oidc {
enabled
allowRegistration
targetFilter {
admin
stream
}
name
clientID
clientSecret
authorizationURL
tokenURL
jwksURI
issuer
}
}
}
`,
authReadOnly: graphql`
fragment OIDCConfigListContainer_authReadOnly on Auth {
integrations {
oidc {
id
callbackURL
}
}
}
`,
})(
withCreateOIDCAuthIntegrationMutation(
withUpdateOIDCAuthIntegrationMutation(
withSubmitHookContext(addSubmitHook => ({ addSubmitHook }))(
OIDCConfigListContainer
)
)
)
);
export default enhanced;
@@ -0,0 +1,60 @@
import React from "react";
import { graphql } from "react-relay";
import { SSOConfigContainer_auth as AuthData } from "talk-admin/__generated__/SSOConfigContainer_auth.graphql";
import { SSOConfigContainer_authReadOnly as AuthReadOnlyData } from "talk-admin/__generated__/SSOConfigContainer_authReadOnly.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import SSOConfig from "../components/SSOConfig";
interface Props {
auth: AuthData;
authReadOnly: AuthReadOnlyData;
onInitValues: (values: AuthData) => void;
disabled?: boolean;
}
class SSOConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.auth);
}
public render() {
const { disabled } = this.props;
return (
<SSOConfig
disabled={disabled}
sso={this.props.authReadOnly.integrations.sso}
/>
);
}
}
const enhanced = withFragmentContainer<Props>({
auth: graphql`
fragment SSOConfigContainer_auth on Auth {
integrations {
sso {
enabled
allowRegistration
targetFilter {
admin
stream
}
}
}
}
`,
authReadOnly: graphql`
fragment SSOConfigContainer_authReadOnly on Auth {
integrations {
sso {
...SSOKeyFieldContainer_sso
}
}
}
`,
})(SSOConfigContainer);
export default enhanced;
@@ -0,0 +1,58 @@
import React from "react";
import { graphql } from "react-relay";
import { SSOKeyFieldContainer_sso as SSOData } from "talk-admin/__generated__/SSOKeyFieldContainer_sso.graphql";
import {
RegenerateSSOKeyMutation,
withRegenerateSSOKeyMutation,
} from "talk-admin/mutations";
import { withFragmentContainer } from "talk-framework/lib/relay";
import SSOKeyField from "../components/SSOKeyField";
interface Props {
sso: SSOData;
disabled?: boolean;
regenerateSSOKey: RegenerateSSOKeyMutation;
}
interface State {
awaitingResponse: boolean;
}
class SSOKeyFieldContainer extends React.Component<Props, State> {
public state = {
awaitingResponse: false,
};
private handleRegenerate = async () => {
this.setState({ awaitingResponse: true });
await this.props.regenerateSSOKey();
this.setState({ awaitingResponse: false });
};
public render() {
const { disabled } = this.props;
return (
<SSOKeyField
disabled={disabled || this.state.awaitingResponse}
generatedKey={this.props.sso.key || undefined}
keyGeneratedAt={this.props.sso.keyGeneratedAt || undefined}
onRegenerate={this.handleRegenerate}
/>
);
}
}
const enhanced = withRegenerateSSOKeyMutation(
withFragmentContainer<Props>({
sso: graphql`
fragment SSOKeyFieldContainer_sso on SSOAuthIntegration {
key
keyGeneratedAt
}
`,
})(SSOKeyFieldContainer)
);
export default enhanced;
@@ -0,0 +1,14 @@
import { noop } from "lodash";
import React from "react";
export type SubmitHook = (data: any) => Promise<any> | any;
export type RemoveSubmitHook = () => void;
export type AddSubmitHook = (hook: SubmitHook) => RemoveSubmitHook;
export type SubmitHookContext = AddSubmitHook;
const { Provider, Consumer } = React.createContext<SubmitHookContext>(
() => noop
);
export const SubmitHookContextProvider = Provider;
export const SubmitHookContextConsumer = Consumer;
@@ -0,0 +1,9 @@
export {
AddSubmitHook,
SubmitHook,
RemoveSubmitHook,
SubmitHookContext,
SubmitHookContextConsumer,
SubmitHookContextProvider,
} from "./SubmitHookContext";
export { default as withSubmitHookContext } from "./withSubmitHookContext";
@@ -0,0 +1,12 @@
import { createContextHOC } from "talk-framework/helpers";
import {
SubmitHookContext,
SubmitHookContextConsumer,
} from "./SubmitHookContext";
const withSubmitHookContext = createContextHOC<SubmitHookContext>(
"withSubmitHookContext",
SubmitHookContextConsumer
);
export default withSubmitHookContext;
@@ -195,7 +195,6 @@ exports[`accepts correct password 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -405,7 +404,6 @@ exports[`accepts valid email 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -628,7 +626,6 @@ exports[`checks for invalid email 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -825,7 +822,6 @@ exports[`renders sign in form 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1048,7 +1044,6 @@ exports[`shows error when submitting empty form 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1245,7 +1240,6 @@ exports[`shows server error 1`] = `
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1447,7 +1441,6 @@ exports[`shows server error 2`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1644,7 +1637,6 @@ exports[`submits form successfully 1`] = `
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1841,7 +1833,6 @@ exports[`submits form successfully 2`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,287 @@
import { cloneDeep, get, merge } from "lodash";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import {
createSinonStub,
inputPredicate,
limitSnapshotTo,
replaceHistoryLocation,
} from "talk-framework/testHelpers";
import create from "../create";
import { settings } from "../fixtures";
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/configure/auth");
});
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
settings: sinon
.stub()
.returns(merge(settings, get(resolver, "Query.settings"))),
},
};
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(true, "loggedIn");
},
});
await timeout();
return testRenderer;
};
it("renders configure auth", async () => {
const testRenderer = await createTestRenderer();
expect(
limitSnapshotTo("configure-container", testRenderer.toJSON())
).toMatchSnapshot();
});
it("regenerate sso key", async () => {
const testRenderer = await createTestRenderer({
Mutation: {
regenerateSSOKey: createSinonStub(s =>
s.callsFake((_: any, data: any) => {
return {
settings: {
auth: {
integrations: {
sso: {
key: "==GENERATED_KEY==",
keyGeneratedAt: "2018-11-12T23:26:06.239Z",
},
},
},
},
clientMutationId: data.input.clientMutationId,
};
})
),
},
});
testRenderer.root
.find(inputPredicate("auth.integrations.sso.enabled"))
.props.onChange({});
testRenderer.root
.find(inputPredicate("configure-auth-sso-regenerate"))
.props.onClick();
await timeout();
expect(
limitSnapshotTo("configure-auth-sso-key", testRenderer.toJSON())
).toMatchSnapshot();
});
it("change settings", async () => {
let settingsRecord = cloneDeep(settings);
const testRenderer = await createTestRenderer({
Query: {
discoverOIDCConfiguration: createSinonStub(s =>
s.callsFake((_: any, data: any) => {
expect(data).toEqual({ issuer: "http://issuer.com" });
return {
issuer: "http://issuer.com",
tokenURL: "http://issuer.com/tokenURL",
jwksURI: "http://issuer.com/jwksURI",
authorizationURL: "http://issuer.com/authorizationURL",
};
})
),
},
Mutation: {
updateSettings: createSinonStub(s =>
s.callsFake((_: any, data: any) => {
expect(data.input.settings.auth.integrations.facebook).toEqual({
enabled: true,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
},
clientID: "myClientID",
clientSecret: "myClientSecret",
});
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
),
createOIDCAuthIntegration: createSinonStub(s =>
s.callsFake((_: any, data: any) => {
expect(data.input.configuration).toEqual({
enabled: true,
allowRegistration: false,
targetFilter: {
admin: true,
stream: true,
},
name: "name",
clientID: "clientID",
clientSecret: "clientSecret",
issuer: "http://issuer.com",
jwksURI: "http://issuer.com/jwksURI",
authorizationURL: "http://issuer.com/authorizationURL",
tokenURL: "http://issuer.com/tokenURL",
});
(settingsRecord.auth.integrations.oidc as any).push({
id: "generatedID",
enabled: false,
callbackURL: "http://localhost/oidc/callback",
...data.input.configuration,
});
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
),
updateOIDCAuthIntegration: createSinonStub(s =>
s.callsFake((_: any, data: any) => {
expect(data.input.configuration).toEqual({
enabled: true,
allowRegistration: false,
targetFilter: {
admin: true,
stream: true,
},
name: "name",
clientID: "clientID",
clientSecret: "clientSecret2",
issuer: "http://issuer.com",
jwksURI: "http://issuer.com/jwksURI",
authorizationURL: "http://issuer.com/authorizationURL",
tokenURL: "http://issuer.com/tokenURL",
});
(settingsRecord.auth.integrations.oidc[0] as any) = merge(
settingsRecord.auth.integrations.oidc[0],
data.input.configuration
);
return {
integration: settingsRecord.auth.integrations.oidc[0],
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
),
},
});
// Let's change some facebook settings.
testRenderer.root
.find(inputPredicate("auth.integrations.facebook.enabled"))
.props.onChange({});
testRenderer.root
.find(inputPredicate("auth.integrations.facebook.clientID"))
.props.onChange("myClientID");
testRenderer.root
.find(inputPredicate("auth.integrations.facebook.clientSecret"))
.props.onChange("myClientSecret");
expect(
limitSnapshotTo("configure-auth-facebook-container", testRenderer.toJSON())
).toMatchSnapshot("enable facebook configure box");
// Send form, this will perform creating an initial oidc record and update settings.
testRenderer.root.findByProps({ id: "configure-form" }).props.onSubmit();
// Submit button should be disabled.
expect(
testRenderer.root.find(inputPredicate("configure-sideBar-saveChanges"))
.props.disabled
).toBe(true);
// Disable other fields while submitting
// We are only testing for one here right now..
expect(
testRenderer.root.find(inputPredicate("auth.integrations.facebook.enabled"))
.props.disabled
).toBe(true);
await timeout();
expect(
testRenderer.root.find(inputPredicate("auth.integrations.facebook.enabled"))
.props.disabled
).toBe(false);
// Now let's enable oidc
testRenderer.root
.find(inputPredicate("auth.integrations.oidc.0.enabled"))
.props.onChange({});
expect(
limitSnapshotTo("configure-auth-oidc-container-0", testRenderer.toJSON())
).toMatchSnapshot("enable oidc configure box");
// Try to submit form, this will give validation error messages.
testRenderer.root.findByProps({ id: "configure-form" }).props.onSubmit();
expect(
limitSnapshotTo("configure-auth-oidc-container-0", testRenderer.toJSON())
).toMatchSnapshot("oidc validation errors");
// Fill form
testRenderer.root
.find(inputPredicate("auth.integrations.oidc.0.name"))
.props.onChange("name");
testRenderer.root
.find(inputPredicate("auth.integrations.oidc.0.clientID"))
.props.onChange("clientID");
testRenderer.root
.find(inputPredicate("auth.integrations.oidc.0.clientSecret"))
.props.onChange("clientSecret");
testRenderer.root
.find(inputPredicate("auth.integrations.oidc.0.issuer"))
.props.onChange("http://issuer.com");
// Discover the rest.
testRenderer.root
.find(inputPredicate("configure-auth-oidc-discover-0"))
.props.onClick();
await timeout();
// Try to submit again, this should work now.
testRenderer.root.findByProps({ id: "configure-form" }).props.onSubmit();
expect(
limitSnapshotTo("configure-auth-oidc-container-0", testRenderer.toJSON())
).toMatchSnapshot("during submit: oidc without errors");
// Disable other fields while submitting
// We are only testing for one here right now..
expect(
testRenderer.root.find(inputPredicate("auth.integrations.oidc.0.enabled"))
.props.disabled
).toBe(true);
await timeout();
expect(
testRenderer.root.find(inputPredicate("auth.integrations.oidc.0.enabled"))
.props.disabled
).toBe(false);
// Change clientSecret
testRenderer.root
.find(inputPredicate("auth.integrations.oidc.0.clientSecret"))
.props.onChange("clientSecret2");
testRenderer.root.findByProps({ id: "configure-form" }).props.onSubmit();
// Disable other fields while submitting
// We are only testing for one here right now..
expect(
testRenderer.root.find(inputPredicate("auth.integrations.oidc.0.enabled"))
.props.disabled
).toBe(true);
await timeout();
expect(
testRenderer.root.find(inputPredicate("auth.integrations.oidc.0.enabled"))
.props.disabled
).toBe(false);
});
+50
View File
@@ -0,0 +1,50 @@
export const settings = {
auth: {
displayName: {
enabled: false,
},
integrations: {
oidc: [],
local: {
enabled: false,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
},
},
sso: {
enabled: false,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
},
key: null,
keyGeneratedAt: null,
},
google: {
enabled: false,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
},
clientID: "",
clientSecret: "",
callbackURL: "http://localhost/google/callback",
},
facebook: {
enabled: false,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
},
clientID: "",
clientSecret: "",
callbackURL: "http://localhost/facebook/callback",
},
},
},
};
@@ -23,9 +23,9 @@ it("redirect to redirectPath when already logged in", async () => {
logNetwork: false,
initLocalState: localRecord => {
localRecord.setValue(true, "loggedIn");
localRecord.setValue("/admin/configure", "redirectPath");
localRecord.setValue("/admin/community", "redirectPath");
},
});
await timeout();
expect(window.location.toString()).toBe("http://localhost/admin/configure");
expect(window.location.toString()).toBe("http://localhost/admin/community");
});
+1 -1
View File
@@ -1,5 +1,5 @@
import React, { StatelessComponent } from "react";
import * as styles from "./App.css";
import styles from "./App.css";
import ForgotPasswordContainer from "../containers/ForgotPasswordContainer";
import ResetPasswordContainer from "../containers/ResetPasswordContainer";
@@ -45,7 +45,6 @@ reset your password.
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -119,7 +118,6 @@ exports[`navigates to sign in form 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -134,7 +132,6 @@ exports[`navigates to sign in form 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -158,7 +155,6 @@ exports[`navigates to sign in form 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -276,7 +272,6 @@ exports[`navigates to sign up form 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -300,7 +295,6 @@ exports[`navigates to sign up form 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -376,7 +370,6 @@ exports[`renders sign in form 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -391,7 +384,6 @@ exports[`renders sign in form 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -415,7 +407,6 @@ exports[`renders sign in form 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -74,7 +74,6 @@ exports[`accepts correct password 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -89,7 +88,6 @@ exports[`accepts correct password 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -113,7 +111,6 @@ exports[`accepts correct password 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -202,7 +199,6 @@ exports[`accepts valid email 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -217,7 +213,6 @@ exports[`accepts valid email 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -241,7 +236,6 @@ exports[`accepts valid email 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -343,7 +337,6 @@ exports[`checks for invalid email 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -358,7 +351,6 @@ exports[`checks for invalid email 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -382,7 +374,6 @@ exports[`checks for invalid email 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -458,7 +449,6 @@ exports[`renders sign in form 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -473,7 +463,6 @@ exports[`renders sign in form 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -497,7 +486,6 @@ exports[`renders sign in form 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -599,7 +587,6 @@ exports[`shows error when submitting empty form 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -614,7 +601,6 @@ exports[`shows error when submitting empty form 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -638,7 +624,6 @@ exports[`shows error when submitting empty form 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -714,7 +699,6 @@ exports[`shows server error 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -729,7 +713,6 @@ exports[`shows server error 1`] = `
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -753,7 +736,6 @@ exports[`shows server error 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -834,7 +816,6 @@ exports[`shows server error 2`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -849,7 +830,6 @@ exports[`shows server error 2`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -873,7 +853,6 @@ exports[`shows server error 2`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -949,7 +928,6 @@ exports[`submits form successfully 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -964,7 +942,6 @@ exports[`submits form successfully 1`] = `
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -988,7 +965,6 @@ exports[`submits form successfully 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1064,7 +1040,6 @@ exports[`submits form successfully 2`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1079,7 +1054,6 @@ exports[`submits form successfully 2`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1103,7 +1077,6 @@ exports[`submits form successfully 2`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -142,7 +142,6 @@ exports[`accepts correct password 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -166,7 +165,6 @@ exports[`accepts correct password 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -336,7 +334,6 @@ exports[`accepts correct password confirmation 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -360,7 +357,6 @@ exports[`accepts correct password confirmation 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -517,7 +513,6 @@ exports[`accepts valid email 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -541,7 +536,6 @@ exports[`accepts valid email 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -698,7 +692,6 @@ exports[`accepts valid username 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -722,7 +715,6 @@ exports[`accepts valid username 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -892,7 +884,6 @@ exports[`checks for invalid characters in username 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -916,7 +907,6 @@ exports[`checks for invalid characters in username 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1086,7 +1076,6 @@ exports[`checks for invalid email 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1110,7 +1099,6 @@ exports[`checks for invalid email 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1280,7 +1268,6 @@ exports[`checks for too long username 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1304,7 +1291,6 @@ exports[`checks for too long username 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1474,7 +1460,6 @@ exports[`checks for too short password 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1498,7 +1483,6 @@ exports[`checks for too short password 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1668,7 +1652,6 @@ exports[`checks for too short username 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1692,7 +1675,6 @@ exports[`checks for too short username 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1862,7 +1844,6 @@ exports[`checks for wrong password confirmation 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -1886,7 +1867,6 @@ exports[`checks for wrong password confirmation 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2004,7 +1984,6 @@ exports[`renders sign up form 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2028,7 +2007,6 @@ exports[`renders sign up form 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2198,7 +2176,6 @@ exports[`shows error when submitting empty form 1`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2222,7 +2199,6 @@ exports[`shows error when submitting empty form 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2340,7 +2316,6 @@ exports[`shows server error 1`] = `
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2364,7 +2339,6 @@ exports[`shows server error 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2487,7 +2461,6 @@ exports[`shows server error 2`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2511,7 +2484,6 @@ exports[`shows server error 2`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2629,7 +2601,6 @@ exports[`submits form successfully 1`] = `
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2653,7 +2624,6 @@ exports[`submits form successfully 1`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2771,7 +2741,6 @@ exports[`submits form successfully 2`] = `
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
@@ -2795,7 +2764,6 @@ exports[`submits form successfully 2`] = `
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
+27 -26
View File
@@ -1,30 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title>Talk 5.0 Embed Stream</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<style>
body {
margin: 0;
padding: 0 20px 50px 20px;
}
</style>
</head>
<head>
<title>Talk 5.0 Embed Stream</title>
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no">
<style>
body {
margin: 0;
padding: 0 20px 50px 20px;
}
</style>
</head>
<body>
<p style="text-align: center">
<a href="/story.html">Story</a> | <a href="/storyButton.html">Story With Button</a>
</p>
<h1 style="text-align: center">Talk 5.0 Embed Stream</h1>
<div id="coralStreamEmbed" style="max-width: 640px; margin: 0 auto"></div>
<script>
const TalkStreamEmbed = Coral.Talk.createStreamEmbed({ id: 'coralStreamEmbed' });
window.TalkStreamEmbed = TalkStreamEmbed;
TalkStreamEmbed.render();
</script>
</body>
<body>
<p style="text-align: center">
<a href="/admin">Admin</a> | <a href="/story.html">Story</a> |
<a href="/storyButton.html">Story With Button</a>
</p>
<h1 style="text-align: center">Talk 5.0 Embed Stream</h1>
<div id="coralStreamEmbed" style="max-width: 640px; margin: 0 auto"></div>
<script>
const TalkStreamEmbed = Coral.Talk.createStreamEmbed({
id: "coralStreamEmbed",
});
window.TalkStreamEmbed = TalkStreamEmbed;
TalkStreamEmbed.render();
</script>
</body>
</html>
+124 -84
View File
@@ -1,88 +1,128 @@
<!DOCTYPE html>
<html>
<head>
<title>Talk 5.0 Embed Stream</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<style>
body {
margin: 0;
padding: 0 100px 50px 100px;
}
</style>
</head>
<head>
<title>Talk 5.0 Embed Stream</title>
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no">
<style>
body {
margin: 0;
padding: 0 100px 50px 100px;
}
</style>
</head>
<body>
<p style="text-align: center">
<a href="/">Default</a> | <a href="/storyButton.html">Story With Button</a>
</p>
<h1 style="text-align: center">Talk 5.0 Story</h1>
<p>Dismember a mouse and then regurgitate parts of it on the family room floor. Dont wait for the storm to pass,
dance in the rain stand in front of the computer screen, so stares at human while pushing stuff off a table chew
the plant meow hiss at vacuum cleaner. Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not
sorry chew the plant. Litter kitter kitty litty little kitten big roar roar feed me rub whiskers on bare skin act
innocent sleep on keyboard, so give me attention or face the wrath of my claws for demand to be let outside at
once, and expect owner to wait for me as i think about it spread kitty litter all over house so nya nya nyan. Catty
ipsum massacre a bird in the living room and then look like the cutest and most innocent animal on the planet you
have cat to be kitten me right meow. Hiss and stare at nothing then run suddenly away refuse to come home when
humans are going to bed; stay out all night then yowl like i am dying at 4am and lick plastic bags. Chase dog then
run away purrr purr littel cat, little cat purr purr and step on your keyboard while you're gaming and then turn in
a circle . Twitch tail in permanent irritation put butt in owner's face and the dog smells bad yet attempt to leap
between furniture but woefully miscalibrate and bellyflop onto the floor; what's your problem? i meant to do that
now i shall wash myself intently. Sniff all the things groom forever, stretch tongue and leave it slightly out,
blep, but bring your owner a dead bird decide to want nothing to do with my owner today for lay on arms while
you're using the keyboard meow meow, i tell my human or scratch. Sleep on my human's head then cats take over the
world bleghbleghvomit my furball really tie the room together sleep more napping, more napping all the napping is
exhausting. When in doubt, wash drink water out of the faucet, cats are fats i like to pets them they like to meow
back and cat dog hate mouse eat string barf pillow no baths hate everything yet swat at dog kitty kitty but you
call this cat food. Cough furball into food bowl then scratch owner for a new one flex claws on the human's belly
and purr like a lawnmower for has closed eyes but still sees you groom yourself 4 hours - checked, have your beauty
sleep 18 hours - checked, be fabulous for the rest of the day - checked. Freak human out make funny noise mow mow
mow mow mow mow success now attack human flex claws on the human's belly and purr like a lawnmower or meowwww.
Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not sorry paw at your fat belly so yowling
nonstop the whole night small kitty warm kitty little balls of fur or eat owner's food reward the chosen human with
a slow blink. Gate keepers of hell plan steps for world domination for more napping, more napping all the napping
is exhausting give me some of your food give me some of your food give me some of your food meh, i don't want it so
flop over. Make meme, make cute face ears back wide eyed so sit and stare. Dead stare with ears cocked furrier and
even more furrier hairball. Stand in front of the computer screen demand to have some of whatever the human is
cooking, then sniff the offering and walk away for catasstrophe, kitty scratches couch bad kitty. Wack the mini
furry mouse intrigued by the shower, and pooping rainbow while flying in a toasted bread costume in space.
Mesmerizing birds love me! shake treat bag, yet lies down where is my slave? I'm getting hungry so lick face hiss
at owner, pee a lot, and meow repeatedly scratch at fence purrrrrr eat muffins and poutine until owner comes back.
You have cat to be kitten me right meow sniff other cat's butt and hang jaw half open thereafter but run outside as
soon as door open so munch on tasty moths or munch on tasty moths, for paw at beetle and eat it before it gets
away. Sit on human. Gnaw the corn cob massacre a bird in the living room and then look like the cutest and most
innocent animal on the planet for sit on the laptop. Meow scratch leg; meow for can opener to feed me cat fur is
the new black but hide when guests come over, and Gate keepers of hell. Refuse to come home when humans are going
to bed; stay out all night then yowl like i am dying at 4am cat slap dog in face or eat a rug and furry furry hairs
everywhere oh no human coming lie on counter don't get off counter for i like fish sit on human they not getting up
ever but meow meow but cuddle no cuddle cuddle love scratch scratch.</p>
<p>I show my fluffy belly but it's a trap! if you pet it i will tear up your hand refuse to drink water except out of
someone's glass mice, so cough hairball, eat toilet paper or curl into a furry donut lick sellotape but wack the
mini furry mouse. When owners are asleep, cry for no apparent reason. Chase imaginary bugs. Stinky cat reward the
chosen human with a slow blink, or chase dog then run away. Chew on cable scratch the furniture for you are a
captive audience while sitting on the toilet, pet me for i like cats because they are fat and fluffy and spend all
night ensuring people don't sleep sleep all day. Scoot butt on the rug need to check on human, have not seen in an
hour might be dead oh look, human is alive, hiss at human, feed me, leave fur on owners clothes, so instantly break
out into full speed gallop across the house for no reason play riveting piece on synthesizer keyboard and scoot
butt on the rug yet meow meow. Attack dog, run away and pretend to be victim annoy the old grumpy cat, start a
fight and then retreat to wash when i lose or meow go back to sleep owner brings food and water tries to pet on
head, so scratch get sprayed by water because bad cat. Meowwww pelt around the house and up and down stairs chasing
phantoms drink water out of the faucet meow meow, i tell my human. Destroy couch.</p>
<p>Ask to go outside and ask to come inside and ask to go outside and ask to come inside the dog smells bad. Lick
butt and make a weird face. Toilet paper attack claws fluff everywhere meow miao french ciao litterbox. Shake treat
bag immediately regret falling into bathtub or white cat sleeps on a black shirt so what a cat-ass-trophy! eat
owner's food spit up on light gray carpet instead of adjacent linoleum. Warm up laptop with butt lick butt fart
rainbows until owner yells pee in litter box hiss at cats scratch the box so loved it, hated it, loved it, hated it
but need to check on human, have not seen in an hour might be dead oh look, human is alive, hiss at human, feed me.
</p>
<div id="coralStreamEmbed" style="max-width: 640px; margin: 0 auto"></div>
<script>
const TalkStreamEmbed = Coral.Talk.createStreamEmbed({ id: 'coralStreamEmbed', autoRender: true });
window.TalkStreamEmbed = TalkStreamEmbed;
</script>
</body>
<body>
<p style="text-align: center">
<a href="/admin">Admin</a> | <a href="/">Default</a> |
<a href="/storyButton.html">Story With Button</a>
</p>
<h1 style="text-align: center">Talk 5.0 Story</h1>
<p>
Dismember a mouse and then regurgitate parts of it on the family room
floor. Dont wait for the storm to pass, dance in the rain stand in front
of the computer screen, so stares at human while pushing stuff off a table
chew the plant meow hiss at vacuum cleaner. Terrorize the
hundred-and-twenty-pound rottweiler and steal his bed, not sorry chew the
plant. Litter kitter kitty litty little kitten big roar roar feed me rub
whiskers on bare skin act innocent sleep on keyboard, so give me attention
or face the wrath of my claws for demand to be let outside at once, and
expect owner to wait for me as i think about it spread kitty litter all
over house so nya nya nyan. Catty ipsum massacre a bird in the living room
and then look like the cutest and most innocent animal on the planet you
have cat to be kitten me right meow. Hiss and stare at nothing then run
suddenly away refuse to come home when humans are going to bed; stay out
all night then yowl like i am dying at 4am and lick plastic bags. Chase
dog then run away purrr purr littel cat, little cat purr purr and step on
your keyboard while you're gaming and then turn in a circle . Twitch tail
in permanent irritation put butt in owner's face and the dog smells bad
yet attempt to leap between furniture but woefully miscalibrate and
bellyflop onto the floor; what's your problem? i meant to do that now i
shall wash myself intently. Sniff all the things groom forever, stretch
tongue and leave it slightly out, blep, but bring your owner a dead bird
decide to want nothing to do with my owner today for lay on arms while
you're using the keyboard meow meow, i tell my human or scratch. Sleep on
my human's head then cats take over the world bleghbleghvomit my furball
really tie the room together sleep more napping, more napping all the
napping is exhausting. When in doubt, wash drink water out of the faucet,
cats are fats i like to pets them they like to meow back and cat dog hate
mouse eat string barf pillow no baths hate everything yet swat at dog
kitty kitty but you call this cat food. Cough furball into food bowl then
scratch owner for a new one flex claws on the human's belly and purr like
a lawnmower for has closed eyes but still sees you groom yourself 4 hours
- checked, have your beauty sleep 18 hours - checked, be fabulous for the
rest of the day - checked. Freak human out make funny noise mow mow mow
mow mow mow success now attack human flex claws on the human's belly and
purr like a lawnmower or meowwww. Terrorize the hundred-and-twenty-pound
rottweiler and steal his bed, not sorry paw at your fat belly so yowling
nonstop the whole night small kitty warm kitty little balls of fur or eat
owner's food reward the chosen human with a slow blink. Gate keepers of
hell plan steps for world domination for more napping, more napping all
the napping is exhausting give me some of your food give me some of your
food give me some of your food meh, i don't want it so flop over. Make
meme, make cute face ears back wide eyed so sit and stare. Dead stare with
ears cocked furrier and even more furrier hairball. Stand in front of the
computer screen demand to have some of whatever the human is cooking, then
sniff the offering and walk away for catasstrophe, kitty scratches couch
bad kitty. Wack the mini furry mouse intrigued by the shower, and pooping
rainbow while flying in a toasted bread costume in space. Mesmerizing
birds love me! shake treat bag, yet lies down where is my slave? I'm
getting hungry so lick face hiss at owner, pee a lot, and meow repeatedly
scratch at fence purrrrrr eat muffins and poutine until owner comes back.
You have cat to be kitten me right meow sniff other cat's butt and hang
jaw half open thereafter but run outside as soon as door open so munch on
tasty moths or munch on tasty moths, for paw at beetle and eat it before
it gets away. Sit on human. Gnaw the corn cob massacre a bird in the
living room and then look like the cutest and most innocent animal on the
planet for sit on the laptop. Meow scratch leg; meow for can opener to
feed me cat fur is the new black but hide when guests come over, and Gate
keepers of hell. Refuse to come home when humans are going to bed; stay
out all night then yowl like i am dying at 4am cat slap dog in face or eat
a rug and furry furry hairs everywhere oh no human coming lie on counter
don't get off counter for i like fish sit on human they not getting up
ever but meow meow but cuddle no cuddle cuddle love scratch scratch.
</p>
<p>
I show my fluffy belly but it's a trap! if you pet it i will tear up your
hand refuse to drink water except out of someone's glass mice, so cough
hairball, eat toilet paper or curl into a furry donut lick sellotape but
wack the mini furry mouse. When owners are asleep, cry for no apparent
reason. Chase imaginary bugs. Stinky cat reward the chosen human with a
slow blink, or chase dog then run away. Chew on cable scratch the
furniture for you are a captive audience while sitting on the toilet, pet
me for i like cats because they are fat and fluffy and spend all night
ensuring people don't sleep sleep all day. Scoot butt on the rug need to
check on human, have not seen in an hour might be dead oh look, human is
alive, hiss at human, feed me, leave fur on owners clothes, so instantly
break out into full speed gallop across the house for no reason play
riveting piece on synthesizer keyboard and scoot butt on the rug yet meow
meow. Attack dog, run away and pretend to be victim annoy the old grumpy
cat, start a fight and then retreat to wash when i lose or meow go back to
sleep owner brings food and water tries to pet on head, so scratch get
sprayed by water because bad cat. Meowwww pelt around the house and up and
down stairs chasing phantoms drink water out of the faucet meow meow, i
tell my human. Destroy couch.
</p>
<p>
Ask to go outside and ask to come inside and ask to go outside and ask to
come inside the dog smells bad. Lick butt and make a weird face. Toilet
paper attack claws fluff everywhere meow miao french ciao litterbox. Shake
treat bag immediately regret falling into bathtub or white cat sleeps on a
black shirt so what a cat-ass-trophy! eat owner's food spit up on light
gray carpet instead of adjacent linoleum. Warm up laptop with butt lick
butt fart rainbows until owner yells pee in litter box hiss at cats
scratch the box so loved it, hated it, loved it, hated it but need to
check on human, have not seen in an hour might be dead oh look, human is
alive, hiss at human, feed me.
</p>
<div id="coralStreamEmbed" style="max-width: 640px; margin: 0 auto"></div>
<script>
const TalkStreamEmbed = Coral.Talk.createStreamEmbed({
id: "coralStreamEmbed",
autoRender: true,
});
window.TalkStreamEmbed = TalkStreamEmbed;
</script>
</body>
</html>
+133 -96
View File
@@ -1,103 +1,140 @@
<!DOCTYPE html>
<html>
<head>
<title>Talk 5.0 Embed Stream</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<style>
body {
margin: 0;
padding: 0 100px 50px 100px;
}
</style>
</head>
<head>
<title>Talk 5.0 Embed Stream</title>
<meta charset="utf-8">
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no">
<style>
body {
margin: 0;
padding: 0 100px 50px 100px;
}
</style>
</head>
<body>
<p style="text-align: center">
<a href="/admin">Admin</a> | <a href="/">Default</a> |
<a href="/story.html">Story</a>
</p>
<h1 style="text-align: center">Talk 5.0 Story with Button</h1>
<p>
Dismember a mouse and then regurgitate parts of it on the family room
floor. Dont wait for the storm to pass, dance in the rain stand in front
of the computer screen, so stares at human while pushing stuff off a table
chew the plant meow hiss at vacuum cleaner. Terrorize the
hundred-and-twenty-pound rottweiler and steal his bed, not sorry chew the
plant. Litter kitter kitty litty little kitten big roar roar feed me rub
whiskers on bare skin act innocent sleep on keyboard, so give me attention
or face the wrath of my claws for demand to be let outside at once, and
expect owner to wait for me as i think about it spread kitty litter all
over house so nya nya nyan. Catty ipsum massacre a bird in the living room
and then look like the cutest and most innocent animal on the planet you
have cat to be kitten me right meow. Hiss and stare at nothing then run
suddenly away refuse to come home when humans are going to bed; stay out
all night then yowl like i am dying at 4am and lick plastic bags. Chase
dog then run away purrr purr littel cat, little cat purr purr and step on
your keyboard while you're gaming and then turn in a circle . Twitch tail
in permanent irritation put butt in owner's face and the dog smells bad
yet attempt to leap between furniture but woefully miscalibrate and
bellyflop onto the floor; what's your problem? i meant to do that now i
shall wash myself intently. Sniff all the things groom forever, stretch
tongue and leave it slightly out, blep, but bring your owner a dead bird
decide to want nothing to do with my owner today for lay on arms while
you're using the keyboard meow meow, i tell my human or scratch. Sleep on
my human's head then cats take over the world bleghbleghvomit my furball
really tie the room together sleep more napping, more napping all the
napping is exhausting. When in doubt, wash drink water out of the faucet,
cats are fats i like to pets them they like to meow back and cat dog hate
mouse eat string barf pillow no baths hate everything yet swat at dog
kitty kitty but you call this cat food. Cough furball into food bowl then
scratch owner for a new one flex claws on the human's belly and purr like
a lawnmower for has closed eyes but still sees you groom yourself 4 hours
- checked, have your beauty sleep 18 hours - checked, be fabulous for the
rest of the day - checked. Freak human out make funny noise mow mow mow
mow mow mow success now attack human flex claws on the human's belly and
purr like a lawnmower or meowwww. Terrorize the hundred-and-twenty-pound
rottweiler and steal his bed, not sorry paw at your fat belly so yowling
nonstop the whole night small kitty warm kitty little balls of fur or eat
owner's food reward the chosen human with a slow blink. Gate keepers of
hell plan steps for world domination for more napping, more napping all
the napping is exhausting give me some of your food give me some of your
food give me some of your food meh, i don't want it so flop over. Make
meme, make cute face ears back wide eyed so sit and stare. Dead stare with
ears cocked furrier and even more furrier hairball. Stand in front of the
computer screen demand to have some of whatever the human is cooking, then
sniff the offering and walk away for catasstrophe, kitty scratches couch
bad kitty. Wack the mini furry mouse intrigued by the shower, and pooping
rainbow while flying in a toasted bread costume in space. Mesmerizing
birds love me! shake treat bag, yet lies down where is my slave? I'm
getting hungry so lick face hiss at owner, pee a lot, and meow repeatedly
scratch at fence purrrrrr eat muffins and poutine until owner comes back.
You have cat to be kitten me right meow sniff other cat's butt and hang
jaw half open thereafter but run outside as soon as door open so munch on
tasty moths or munch on tasty moths, for paw at beetle and eat it before
it gets away. Sit on human. Gnaw the corn cob massacre a bird in the
living room and then look like the cutest and most innocent animal on the
planet for sit on the laptop. Meow scratch leg; meow for can opener to
feed me cat fur is the new black but hide when guests come over, and Gate
keepers of hell. Refuse to come home when humans are going to bed; stay
out all night then yowl like i am dying at 4am cat slap dog in face or eat
a rug and furry furry hairs everywhere oh no human coming lie on counter
don't get off counter for i like fish sit on human they not getting up
ever but meow meow but cuddle no cuddle cuddle love scratch scratch.
</p>
<p>
I show my fluffy belly but it's a trap! if you pet it i will tear up your
hand refuse to drink water except out of someone's glass mice, so cough
hairball, eat toilet paper or curl into a furry donut lick sellotape but
wack the mini furry mouse. When owners are asleep, cry for no apparent
reason. Chase imaginary bugs. Stinky cat reward the chosen human with a
slow blink, or chase dog then run away. Chew on cable scratch the
furniture for you are a captive audience while sitting on the toilet, pet
me for i like cats because they are fat and fluffy and spend all night
ensuring people don't sleep sleep all day. Scoot butt on the rug need to
check on human, have not seen in an hour might be dead oh look, human is
alive, hiss at human, feed me, leave fur on owners clothes, so instantly
break out into full speed gallop across the house for no reason play
riveting piece on synthesizer keyboard and scoot butt on the rug yet meow
meow. Attack dog, run away and pretend to be victim annoy the old grumpy
cat, start a fight and then retreat to wash when i lose or meow go back to
sleep owner brings food and water tries to pet on head, so scratch get
sprayed by water because bad cat. Meowwww pelt around the house and up and
down stairs chasing phantoms drink water out of the faucet meow meow, i
tell my human. Destroy couch.
</p>
<p>
Ask to go outside and ask to come inside and ask to go outside and ask to
come inside the dog smells bad. Lick butt and make a weird face. Toilet
paper attack claws fluff everywhere meow miao french ciao litterbox. Shake
treat bag immediately regret falling into bathtub or white cat sleeps on a
black shirt so what a cat-ass-trophy! eat owner's food spit up on light
gray carpet instead of adjacent linoleum. Warm up laptop with butt lick
butt fart rainbows until owner yells pee in litter box hiss at cats
scratch the box so loved it, hated it, loved it, hated it but need to
check on human, have not seen in an hour might be dead oh look, human is
alive, hiss at human, feed me.
</p>
<div id="coralStreamEmbed" style="max-width: 640px; margin: 0 auto"></div>
<div style="text-align: center">
<button id="showComments">Show Comments</button>
</div>
<script>
const TalkStreamEmbed = Coral.Talk.createStreamEmbed({
id: "coralStreamEmbed",
});
window.TalkStreamEmbed = TalkStreamEmbed;
<body>
<p style="text-align: center">
<a href="/">Default</a> | <a href="/story.html">Story</a>
</p>
<h1 style="text-align: center">Talk 5.0 Story with Button</h1>
<p>Dismember a mouse and then regurgitate parts of it on the family room floor. Dont wait for the storm to pass,
dance in the rain stand in front of the computer screen, so stares at human while pushing stuff off a table chew
the plant meow hiss at vacuum cleaner. Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not
sorry chew the plant. Litter kitter kitty litty little kitten big roar roar feed me rub whiskers on bare skin act
innocent sleep on keyboard, so give me attention or face the wrath of my claws for demand to be let outside at
once, and expect owner to wait for me as i think about it spread kitty litter all over house so nya nya nyan. Catty
ipsum massacre a bird in the living room and then look like the cutest and most innocent animal on the planet you
have cat to be kitten me right meow. Hiss and stare at nothing then run suddenly away refuse to come home when
humans are going to bed; stay out all night then yowl like i am dying at 4am and lick plastic bags. Chase dog then
run away purrr purr littel cat, little cat purr purr and step on your keyboard while you're gaming and then turn in
a circle . Twitch tail in permanent irritation put butt in owner's face and the dog smells bad yet attempt to leap
between furniture but woefully miscalibrate and bellyflop onto the floor; what's your problem? i meant to do that
now i shall wash myself intently. Sniff all the things groom forever, stretch tongue and leave it slightly out,
blep, but bring your owner a dead bird decide to want nothing to do with my owner today for lay on arms while
you're using the keyboard meow meow, i tell my human or scratch. Sleep on my human's head then cats take over the
world bleghbleghvomit my furball really tie the room together sleep more napping, more napping all the napping is
exhausting. When in doubt, wash drink water out of the faucet, cats are fats i like to pets them they like to meow
back and cat dog hate mouse eat string barf pillow no baths hate everything yet swat at dog kitty kitty but you
call this cat food. Cough furball into food bowl then scratch owner for a new one flex claws on the human's belly
and purr like a lawnmower for has closed eyes but still sees you groom yourself 4 hours - checked, have your beauty
sleep 18 hours - checked, be fabulous for the rest of the day - checked. Freak human out make funny noise mow mow
mow mow mow mow success now attack human flex claws on the human's belly and purr like a lawnmower or meowwww.
Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not sorry paw at your fat belly so yowling
nonstop the whole night small kitty warm kitty little balls of fur or eat owner's food reward the chosen human with
a slow blink. Gate keepers of hell plan steps for world domination for more napping, more napping all the napping
is exhausting give me some of your food give me some of your food give me some of your food meh, i don't want it so
flop over. Make meme, make cute face ears back wide eyed so sit and stare. Dead stare with ears cocked furrier and
even more furrier hairball. Stand in front of the computer screen demand to have some of whatever the human is
cooking, then sniff the offering and walk away for catasstrophe, kitty scratches couch bad kitty. Wack the mini
furry mouse intrigued by the shower, and pooping rainbow while flying in a toasted bread costume in space.
Mesmerizing birds love me! shake treat bag, yet lies down where is my slave? I'm getting hungry so lick face hiss
at owner, pee a lot, and meow repeatedly scratch at fence purrrrrr eat muffins and poutine until owner comes back.
You have cat to be kitten me right meow sniff other cat's butt and hang jaw half open thereafter but run outside as
soon as door open so munch on tasty moths or munch on tasty moths, for paw at beetle and eat it before it gets
away. Sit on human. Gnaw the corn cob massacre a bird in the living room and then look like the cutest and most
innocent animal on the planet for sit on the laptop. Meow scratch leg; meow for can opener to feed me cat fur is
the new black but hide when guests come over, and Gate keepers of hell. Refuse to come home when humans are going
to bed; stay out all night then yowl like i am dying at 4am cat slap dog in face or eat a rug and furry furry hairs
everywhere oh no human coming lie on counter don't get off counter for i like fish sit on human they not getting up
ever but meow meow but cuddle no cuddle cuddle love scratch scratch.</p>
<p>I show my fluffy belly but it's a trap! if you pet it i will tear up your hand refuse to drink water except out of
someone's glass mice, so cough hairball, eat toilet paper or curl into a furry donut lick sellotape but wack the
mini furry mouse. When owners are asleep, cry for no apparent reason. Chase imaginary bugs. Stinky cat reward the
chosen human with a slow blink, or chase dog then run away. Chew on cable scratch the furniture for you are a
captive audience while sitting on the toilet, pet me for i like cats because they are fat and fluffy and spend all
night ensuring people don't sleep sleep all day. Scoot butt on the rug need to check on human, have not seen in an
hour might be dead oh look, human is alive, hiss at human, feed me, leave fur on owners clothes, so instantly break
out into full speed gallop across the house for no reason play riveting piece on synthesizer keyboard and scoot
butt on the rug yet meow meow. Attack dog, run away and pretend to be victim annoy the old grumpy cat, start a
fight and then retreat to wash when i lose or meow go back to sleep owner brings food and water tries to pet on
head, so scratch get sprayed by water because bad cat. Meowwww pelt around the house and up and down stairs chasing
phantoms drink water out of the faucet meow meow, i tell my human. Destroy couch.</p>
<p>Ask to go outside and ask to come inside and ask to go outside and ask to come inside the dog smells bad. Lick
butt and make a weird face. Toilet paper attack claws fluff everywhere meow miao french ciao litterbox. Shake treat
bag immediately regret falling into bathtub or white cat sleeps on a black shirt so what a cat-ass-trophy! eat
owner's food spit up on light gray carpet instead of adjacent linoleum. Warm up laptop with butt lick butt fart
rainbows until owner yells pee in litter box hiss at cats scratch the box so loved it, hated it, loved it, hated it
but need to check on human, have not seen in an hour might be dead oh look, human is alive, hiss at human, feed me.
</p>
<div id="coralStreamEmbed" style="max-width: 640px; margin: 0 auto"></div>
<div style="text-align: center">
<button id="showComments">Show Comments</button>
</div>
<script>
const TalkStreamEmbed = Coral.Talk.createStreamEmbed({
id: 'coralStreamEmbed',
});
window.TalkStreamEmbed = TalkStreamEmbed;
const button = document.getElementById("showComments");
const button = document.getElementById("showComments");
const showStreamEmbed = () => {
TalkStreamEmbed.render();
button.parentElement.removeChild(button);
};
button.onclick = showStreamEmbed;
TalkStreamEmbed.on("showPermalink", showStreamEmbed);
</script>
</body>
const showStreamEmbed = () => {
TalkStreamEmbed.render();
button.parentElement.removeChild(button);
};
button.onclick = showStreamEmbed;
TalkStreamEmbed.on("showPermalink", showStreamEmbed);
</script>
</body>
</html>
@@ -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";

Some files were not shown because too many files have changed in this diff Show More