mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 20:23:30 +08:00
[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:
Generated
+7
-5
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]>;
|
||||
@@ -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"]>;
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
+15
@@ -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>
|
||||
`;
|
||||
+13
@@ -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;
|
||||
+28
@@ -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>
|
||||
`;
|
||||
+8
-6
@@ -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)>
|
||||
`;
|
||||
+7
@@ -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;
|
||||
+69
@@ -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;
|
||||
+54
@@ -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;
|
||||
+94
@@ -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 />,
|
||||
};
|
||||
+37
@@ -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;
|
||||
+62
@@ -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;
|
||||
+62
@@ -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;
|
||||
+44
@@ -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;
|
||||
+70
@@ -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;
|
||||
+150
@@ -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;
|
||||
+60
@@ -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;
|
||||
+58
@@ -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);
|
||||
});
|
||||
@@ -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,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]}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user