Allow disconnecting analytics integrations (#9823)

* Allow disconnecting analytic integrations

* review
This commit is contained in:
Hemachandar
2025-08-05 17:06:48 +05:30
committed by GitHub
parent b028b541a8
commit b4757dfc6c
7 changed files with 250 additions and 76 deletions
+28
View File
@@ -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>
);
});
+51 -19
View File
@@ -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);
+57 -25
View File
@@ -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);
+58 -30
View File
@@ -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;
+5 -2
View File
@@ -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",