mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Allow disconnecting analytics integrations (#9823)
* Allow disconnecting analytic integrations * review
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { TrashIcon } from "outline-icons";
|
||||
import stores from "~/stores";
|
||||
import { createAction } from "..";
|
||||
import { SettingsSection } from "../sections";
|
||||
import Integration from "~/models/Integration";
|
||||
import { IntegrationType } from "@shared/types";
|
||||
import { DisconnectAnalyticsDialog } from "~/components/DisconnectAnalyticsDialog";
|
||||
|
||||
export const disconnectAnalyticsIntegrationFactory = (
|
||||
integration?: Integration<IntegrationType.Analytics>
|
||||
) =>
|
||||
createAction({
|
||||
name: ({ t }) => t("Disconnect analytics"),
|
||||
analyticsName: "Disconnect analytics",
|
||||
section: SettingsSection,
|
||||
icon: <TrashIcon />,
|
||||
keywords: "disconnect",
|
||||
visible: () => !!integration,
|
||||
perform: ({ t, event }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Disconnect analytics"),
|
||||
content: <DisconnectAnalyticsDialog integration={integration!} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
||||
import Text from "~/components/Text";
|
||||
import { IntegrationType } from "@shared/types";
|
||||
import Integration from "~/models/Integration";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { settingsPath } from "~/utils/routeHelpers";
|
||||
import { observer } from "mobx-react";
|
||||
import capitalize from "lodash/capitalize";
|
||||
|
||||
type Props = {
|
||||
integration: Integration<IntegrationType.Analytics>;
|
||||
};
|
||||
|
||||
export const DisconnectAnalyticsDialog = observer(({ integration }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { dialogs } = useStores();
|
||||
const history = useHistory();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await integration.delete();
|
||||
history.push(settingsPath("integrations"));
|
||||
dialogs.closeAllModals();
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
onSubmit={handleSubmit}
|
||||
submitText={t("Disconnect")}
|
||||
savingText={`${t("Disconnecting")}…`}
|
||||
danger
|
||||
>
|
||||
<Text as="p" type="secondary">
|
||||
<Trans
|
||||
defaults="Are you sure you want to disconnect the <em>{{ service }}</em> integration?"
|
||||
values={{
|
||||
service: capitalize(integration.service),
|
||||
}}
|
||||
components={{
|
||||
em: <strong />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
<Text as="p" type="secondary">
|
||||
<Trans defaults="This will stop sending analytics events to the configured instance." />
|
||||
</Text>
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
});
|
||||
@@ -14,6 +14,10 @@ import GoogleIcon from "~/components/Icons/GoogleIcon";
|
||||
import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { disconnectAnalyticsIntegrationFactory } from "~/actions/definitions/integrations";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import Flex from "~/components/Flex";
|
||||
import styled from "styled-components";
|
||||
|
||||
type FormData = {
|
||||
measurementId: string;
|
||||
@@ -22,12 +26,15 @@ type FormData = {
|
||||
function GoogleAnalytics() {
|
||||
const { integrations } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const context = useActionContext();
|
||||
|
||||
const integration = find(integrations.orderedData, {
|
||||
type: IntegrationType.Analytics,
|
||||
service: IntegrationService.GoogleAnalytics,
|
||||
}) as Integration<IntegrationType.Analytics> | undefined;
|
||||
|
||||
const measurementId = integration?.settings.measurementId;
|
||||
|
||||
const {
|
||||
register,
|
||||
reset,
|
||||
@@ -36,29 +43,25 @@ function GoogleAnalytics() {
|
||||
} = useForm<FormData>({
|
||||
mode: "all",
|
||||
defaultValues: {
|
||||
measurementId: integration?.settings.measurementId,
|
||||
measurementId,
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
reset({ measurementId: integration?.settings.measurementId });
|
||||
}, [integration, reset]);
|
||||
reset({ measurementId });
|
||||
}, [reset, measurementId]);
|
||||
|
||||
const handleSubmit = React.useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
if (data.measurementId) {
|
||||
await integrations.save({
|
||||
id: integration?.id,
|
||||
type: IntegrationType.Analytics,
|
||||
service: IntegrationService.GoogleAnalytics,
|
||||
settings: {
|
||||
measurementId: data.measurementId,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await integration?.delete();
|
||||
}
|
||||
await integrations.save({
|
||||
id: integration?.id,
|
||||
type: IntegrationType.Analytics,
|
||||
service: IntegrationService.GoogleAnalytics,
|
||||
settings: {
|
||||
measurementId: data.measurementId,
|
||||
},
|
||||
});
|
||||
|
||||
toast.success(t("Settings saved"));
|
||||
} catch (err) {
|
||||
@@ -87,15 +90,44 @@ function GoogleAnalytics() {
|
||||
)}
|
||||
border={false}
|
||||
>
|
||||
<Input placeholder="G-XXXXXXXXX1" {...register("measurementId")} />
|
||||
<Input
|
||||
placeholder="G-XXXXXXXXX1"
|
||||
{...register("measurementId", { required: true })}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<Button type="submit" disabled={formState.isSubmitting}>
|
||||
{formState.isSubmitting ? `${t("Saving")}…` : t("Save")}
|
||||
</Button>
|
||||
<Actions reverse justify="end" gap={8}>
|
||||
<StyledSubmit
|
||||
type="submit"
|
||||
disabled={
|
||||
!formState.isDirty || !formState.isValid || formState.isSubmitting
|
||||
}
|
||||
>
|
||||
{formState.isSubmitting ? `${t("Saving")}…` : t("Save")}
|
||||
</StyledSubmit>
|
||||
|
||||
<Button
|
||||
action={disconnectAnalyticsIntegrationFactory(integration)}
|
||||
context={context}
|
||||
disabled={formState.isSubmitting}
|
||||
neutral
|
||||
hideIcon
|
||||
hideOnActionDisabled
|
||||
>
|
||||
{t("Disconnect")}
|
||||
</Button>
|
||||
</Actions>
|
||||
</form>
|
||||
</IntegrationScene>
|
||||
);
|
||||
}
|
||||
|
||||
const Actions = styled(Flex)`
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
const StyledSubmit = styled(Button)`
|
||||
width: 80px;
|
||||
`;
|
||||
|
||||
export default observer(GoogleAnalytics);
|
||||
|
||||
@@ -14,6 +14,10 @@ import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import Icon from "./Icon";
|
||||
import { disconnectAnalyticsIntegrationFactory } from "~/actions/definitions/integrations";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import Flex from "~/components/Flex";
|
||||
import styled from "styled-components";
|
||||
|
||||
type FormData = {
|
||||
instanceUrl: string;
|
||||
@@ -23,12 +27,16 @@ type FormData = {
|
||||
function Matomo() {
|
||||
const { integrations } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const context = useActionContext();
|
||||
|
||||
const integration = find(integrations.orderedData, {
|
||||
type: IntegrationType.Analytics,
|
||||
service: IntegrationService.Matomo,
|
||||
}) as Integration<IntegrationType.Analytics> | undefined;
|
||||
|
||||
const instanceUrl = integration?.settings.instanceUrl;
|
||||
const measurementId = integration?.settings.measurementId;
|
||||
|
||||
const {
|
||||
register,
|
||||
reset,
|
||||
@@ -37,35 +45,31 @@ function Matomo() {
|
||||
} = useForm<FormData>({
|
||||
mode: "all",
|
||||
defaultValues: {
|
||||
instanceUrl: integration?.settings.instanceUrl,
|
||||
measurementId: integration?.settings.measurementId,
|
||||
instanceUrl,
|
||||
measurementId,
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
reset({
|
||||
measurementId: integration?.settings.measurementId,
|
||||
instanceUrl: integration?.settings.instanceUrl,
|
||||
instanceUrl,
|
||||
measurementId,
|
||||
});
|
||||
}, [integration, reset]);
|
||||
}, [reset, instanceUrl, measurementId]);
|
||||
|
||||
const handleSubmit = React.useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
if (data.instanceUrl && data.measurementId) {
|
||||
await integrations.save({
|
||||
id: integration?.id,
|
||||
type: IntegrationType.Analytics,
|
||||
service: IntegrationService.Matomo,
|
||||
settings: {
|
||||
measurementId: data.measurementId,
|
||||
// Ensure the URL ends with a trailing slash
|
||||
instanceUrl: data.instanceUrl.replace(/\/?$/, "/"),
|
||||
} as Integration<IntegrationType.Analytics>["settings"],
|
||||
});
|
||||
} else {
|
||||
await integration?.delete();
|
||||
}
|
||||
await integrations.save({
|
||||
id: integration?.id,
|
||||
type: IntegrationType.Analytics,
|
||||
service: IntegrationService.Matomo,
|
||||
settings: {
|
||||
measurementId: data.measurementId,
|
||||
// Ensure the URL ends with a trailing slash
|
||||
instanceUrl: data.instanceUrl.replace(/\/?$/, "/"),
|
||||
} as Integration<IntegrationType.Analytics>["settings"],
|
||||
});
|
||||
|
||||
toast.success(t("Settings saved"));
|
||||
} catch (err) {
|
||||
@@ -95,9 +99,8 @@ function Matomo() {
|
||||
border={false}
|
||||
>
|
||||
<Input
|
||||
required
|
||||
placeholder="https://instance.matomo.cloud/"
|
||||
{...register("instanceUrl")}
|
||||
{...register("instanceUrl", { required: true })}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
@@ -108,15 +111,44 @@ function Matomo() {
|
||||
)}
|
||||
border={false}
|
||||
>
|
||||
<Input required placeholder="1" {...register("measurementId")} />
|
||||
<Input
|
||||
placeholder="1"
|
||||
{...register("measurementId", { required: true })}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<Button type="submit" disabled={formState.isSubmitting}>
|
||||
{formState.isSubmitting ? `${t("Saving")}…` : t("Save")}
|
||||
</Button>
|
||||
<Actions reverse justify="end" gap={8}>
|
||||
<StyledSubmit
|
||||
type="submit"
|
||||
disabled={
|
||||
!formState.isDirty || !formState.isValid || formState.isSubmitting
|
||||
}
|
||||
>
|
||||
{formState.isSubmitting ? `${t("Saving")}…` : t("Save")}
|
||||
</StyledSubmit>
|
||||
|
||||
<Button
|
||||
action={disconnectAnalyticsIntegrationFactory(integration)}
|
||||
context={context}
|
||||
disabled={formState.isSubmitting}
|
||||
neutral
|
||||
hideIcon
|
||||
hideOnActionDisabled
|
||||
>
|
||||
{t("Disconnect")}
|
||||
</Button>
|
||||
</Actions>
|
||||
</form>
|
||||
</IntegrationScene>
|
||||
);
|
||||
}
|
||||
|
||||
const Actions = styled(Flex)`
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
const StyledSubmit = styled(Button)`
|
||||
width: 80px;
|
||||
`;
|
||||
|
||||
export default observer(Matomo);
|
||||
|
||||
@@ -14,6 +14,10 @@ import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import Icon from "./Icon";
|
||||
import Flex from "~/components/Flex";
|
||||
import { disconnectAnalyticsIntegrationFactory } from "~/actions/definitions/integrations";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import styled from "styled-components";
|
||||
|
||||
type FormData = {
|
||||
umamiDomain: string;
|
||||
@@ -24,12 +28,17 @@ type FormData = {
|
||||
function Umami() {
|
||||
const { integrations } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const context = useActionContext();
|
||||
|
||||
const integration = find(integrations.orderedData, {
|
||||
type: IntegrationType.Analytics,
|
||||
service: IntegrationService.Umami,
|
||||
}) as Integration<IntegrationType.Analytics> | undefined;
|
||||
|
||||
const instanceUrl = integration?.settings.instanceUrl;
|
||||
const scriptName = integration?.settings.scriptName;
|
||||
const measurementId = integration?.settings.measurementId;
|
||||
|
||||
const {
|
||||
register,
|
||||
reset,
|
||||
@@ -38,37 +47,33 @@ function Umami() {
|
||||
} = useForm<FormData>({
|
||||
mode: "all",
|
||||
defaultValues: {
|
||||
umamiDomain: integration?.settings.instanceUrl,
|
||||
umamiScriptName: integration?.settings.scriptName,
|
||||
umamiWebsiteId: integration?.settings.measurementId,
|
||||
umamiDomain: instanceUrl,
|
||||
umamiScriptName: scriptName,
|
||||
umamiWebsiteId: measurementId,
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
reset({
|
||||
umamiWebsiteId: integration?.settings.measurementId,
|
||||
umamiDomain: integration?.settings.instanceUrl,
|
||||
umamiScriptName: integration?.settings.scriptName,
|
||||
umamiDomain: instanceUrl,
|
||||
umamiScriptName: scriptName,
|
||||
umamiWebsiteId: measurementId,
|
||||
});
|
||||
}, [integration, reset]);
|
||||
}, [reset, instanceUrl, scriptName, measurementId]);
|
||||
|
||||
const handleSubmit = React.useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
if (data.umamiDomain && data.umamiScriptName && data.umamiWebsiteId) {
|
||||
await integrations.save({
|
||||
id: integration?.id,
|
||||
type: IntegrationType.Analytics,
|
||||
service: IntegrationService.Umami,
|
||||
settings: {
|
||||
measurementId: data.umamiWebsiteId,
|
||||
instanceUrl: data.umamiDomain.replace(/\/?$/, "/"),
|
||||
scriptName: data.umamiScriptName,
|
||||
} as Integration<IntegrationType.Analytics>["settings"],
|
||||
});
|
||||
} else {
|
||||
await integration?.delete();
|
||||
}
|
||||
await integrations.save({
|
||||
id: integration?.id,
|
||||
type: IntegrationType.Analytics,
|
||||
service: IntegrationService.Umami,
|
||||
settings: {
|
||||
measurementId: data.umamiWebsiteId,
|
||||
instanceUrl: data.umamiDomain.replace(/\/?$/, "/"),
|
||||
scriptName: data.umamiScriptName,
|
||||
} as Integration<IntegrationType.Analytics>["settings"],
|
||||
});
|
||||
|
||||
toast.success(t("Settings saved"));
|
||||
} catch (err) {
|
||||
@@ -101,9 +106,8 @@ function Umami() {
|
||||
border={false}
|
||||
>
|
||||
<Input
|
||||
required
|
||||
placeholder="https://cloud.umami.is/"
|
||||
{...register("umamiDomain")}
|
||||
{...register("umamiDomain", { required: true })}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
@@ -115,9 +119,8 @@ function Umami() {
|
||||
border={false}
|
||||
>
|
||||
<Input
|
||||
required
|
||||
placeholder="script.js"
|
||||
{...register("umamiScriptName")}
|
||||
{...register("umamiScriptName", { required: true })}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
@@ -129,18 +132,43 @@ function Umami() {
|
||||
border={false}
|
||||
>
|
||||
<Input
|
||||
required
|
||||
placeholder="xxx-xxx-xxx-xxx"
|
||||
{...register("umamiWebsiteId")}
|
||||
{...register("umamiWebsiteId", { required: true })}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<Button type="submit" disabled={formState.isSubmitting}>
|
||||
{formState.isSubmitting ? `${t("Saving")}…` : t("Save")}
|
||||
</Button>
|
||||
<Actions reverse justify="end" gap={8}>
|
||||
<StyledSubmit
|
||||
type="submit"
|
||||
disabled={
|
||||
!formState.isDirty || !formState.isValid || formState.isSubmitting
|
||||
}
|
||||
>
|
||||
{formState.isSubmitting ? `${t("Saving")}…` : t("Save")}
|
||||
</StyledSubmit>
|
||||
|
||||
<Button
|
||||
action={disconnectAnalyticsIntegrationFactory(integration)}
|
||||
context={context}
|
||||
disabled={formState.isSubmitting}
|
||||
neutral
|
||||
hideIcon
|
||||
hideOnActionDisabled
|
||||
>
|
||||
{t("Disconnect")}
|
||||
</Button>
|
||||
</Actions>
|
||||
</form>
|
||||
</IntegrationScene>
|
||||
);
|
||||
}
|
||||
|
||||
const Actions = styled(Flex)`
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
const StyledSubmit = styled(Button)`
|
||||
width: 80px;
|
||||
`;
|
||||
|
||||
export default observer(Umami);
|
||||
|
||||
@@ -69,6 +69,7 @@ router.post(
|
||||
"integrations.create",
|
||||
auth({ role: UserRole.Admin }),
|
||||
validate(T.IntegrationsCreateSchema),
|
||||
transaction(),
|
||||
async (ctx: APIContext<T.IntegrationsCreateReq>) => {
|
||||
const { type, service, settings } = ctx.input.body;
|
||||
const { user } = ctx.state.auth;
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"Leave document": "Leave document",
|
||||
"You have left the shared document": "You have left the shared document",
|
||||
"Could not leave document": "Could not leave document",
|
||||
"Disconnect analytics": "Disconnect analytics",
|
||||
"Home": "Home",
|
||||
"Drafts": "Drafts",
|
||||
"Search": "Search",
|
||||
@@ -194,6 +195,10 @@
|
||||
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
|
||||
"Start view": "Start view",
|
||||
"Install now": "Install now",
|
||||
"Disconnect": "Disconnect",
|
||||
"Disconnecting": "Disconnecting",
|
||||
"Are you sure you want to disconnect the <em>{{ service }}</em> integration?": "Are you sure you want to disconnect the <em>{{ service }}</em> integration?",
|
||||
"This will stop sending analytics events to the configured instance.": "This will stop sending analytics events to the configured instance.",
|
||||
"Deleted Collection": "Deleted Collection",
|
||||
"Untitled": "Untitled",
|
||||
"Unpin": "Unpin",
|
||||
@@ -941,8 +946,6 @@
|
||||
"Are you sure you want to revoke the {{ tokenName }} token?": "Are you sure you want to revoke the {{ tokenName }} token?",
|
||||
"Disconnect integration": "Disconnect integration",
|
||||
"Connected": "Connected",
|
||||
"Disconnect": "Disconnect",
|
||||
"Disconnecting": "Disconnecting",
|
||||
"Allowed domains": "Allowed domains",
|
||||
"The domains which should be allowed to create new accounts using SSO. Changing this setting does not affect existing user accounts.": "The domains which should be allowed to create new accounts using SSO. Changing this setting does not affect existing user accounts.",
|
||||
"Remove domain": "Remove domain",
|
||||
|
||||
Reference in New Issue
Block a user