feat: better feature flags (#2026)
* feat: feature flag upgrade * fix translations * refactor * fix * fix
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import { Group, Text, Switch, MantineSize, Title } from "@mantine/core";
|
||||
import { Group, Text, Switch, MantineSize, Tooltip } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import useLicense from "@/ee/hooks/use-license.tsx";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
|
||||
export default function EnableAiSearch() {
|
||||
const { t } = useTranslation();
|
||||
@@ -37,9 +38,8 @@ export function AiSearchToggle({ size, label }: AiSearchToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(workspace?.settings?.ai?.search);
|
||||
const { hasLicenseKey } = useLicense();
|
||||
|
||||
const hasAccess = isCloud() || (!isCloud() && hasLicenseKey);
|
||||
const hasAccess = useHasFeature(Feature.AI);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
@@ -56,14 +56,16 @@ export function AiSearchToggle({ size, label }: AiSearchToggleProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
size={size}
|
||||
label={label}
|
||||
labelPosition="left"
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
aria-label={t("Toggle AI search")}
|
||||
/>
|
||||
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
|
||||
<Switch
|
||||
size={size}
|
||||
label={label}
|
||||
labelPosition="left"
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
aria-label={t("Toggle AI search")}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { Group, Text, Switch } from "@mantine/core";
|
||||
import { Group, Text, Switch, Tooltip } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useIsCloudEE } from "@/hooks/use-is-cloud-ee.tsx";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
|
||||
export default function EnableGenerativeAi() {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(workspace?.settings?.ai?.generative);
|
||||
const hasAccess = useIsCloudEE();
|
||||
const hasAccess = useHasFeature(Feature.AI);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
@@ -38,11 +41,13 @@ export default function EnableGenerativeAi() {
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
/>
|
||||
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
|
||||
<Switch
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ import {
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useIsCloudEE } from "@/hooks/use-is-cloud-ee.tsx";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
import { getAppUrl } from "@/lib/config.ts";
|
||||
import { IconCheck, IconCopy, IconInfoCircle } from "@tabler/icons-react";
|
||||
import { CopyButton } from "@/components/common/copy-button.tsx";
|
||||
@@ -25,7 +27,8 @@ export default function McpSettings() {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(workspace?.settings?.ai?.mcp);
|
||||
const hasAccess = useIsCloudEE();
|
||||
const hasAccess = useHasFeature(Feature.MCP);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const mcpUrl = `${getAppUrl()}/mcp`;
|
||||
|
||||
@@ -46,11 +49,7 @@ export default function McpSettings() {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{!hasAccess && (
|
||||
<Alert
|
||||
icon={<IconInfoCircle />}
|
||||
title={t("Enterprise feature")}
|
||||
color="blue"
|
||||
>
|
||||
<Alert icon={<IconInfoCircle />} title={upgradeLabel} color="blue">
|
||||
{t(
|
||||
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
|
||||
)}
|
||||
@@ -64,23 +63,22 @@ export default function McpSettings() {
|
||||
{t(
|
||||
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.",
|
||||
)}{" "}
|
||||
{t("View the")}{" "}
|
||||
<Anchor
|
||||
href="https://docmost.com/docs/user-guide/mcp"
|
||||
target="_blank"
|
||||
size="sm"
|
||||
>
|
||||
{t("MCP documentation")}
|
||||
</Anchor>
|
||||
.
|
||||
<Trans
|
||||
i18nKey="View the <anchor>MCP documentation</anchor>."
|
||||
components={{
|
||||
anchor: <Anchor href="https://docmost.com/docs/user-guide/mcp" target="_blank" size="sm" />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
/>
|
||||
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
|
||||
<Switch
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
{checked && (
|
||||
@@ -89,11 +87,7 @@ export default function McpSettings() {
|
||||
{t("MCP Server URL")}
|
||||
</Text>
|
||||
<Group gap="xs">
|
||||
<TextInput
|
||||
value={mcpUrl}
|
||||
readOnly
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<TextInput value={mcpUrl} readOnly style={{ flex: 1 }} />
|
||||
<CopyButton value={mcpUrl} timeout={2000}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip
|
||||
@@ -123,12 +117,36 @@ export default function McpSettings() {
|
||||
{t("Supported tools")}
|
||||
</Text>
|
||||
<List size="sm" spacing={2}>
|
||||
<List.Item><Text size="sm" c="dimmed" span>search_pages, get_page, create_page, update_page</Text></List.Item>
|
||||
<List.Item><Text size="sm" c="dimmed" span>list_pages, list_child_pages, duplicate_page</Text></List.Item>
|
||||
<List.Item><Text size="sm" c="dimmed" span>copy_page_to_space, move_page, move_page_to_space</Text></List.Item>
|
||||
<List.Item><Text size="sm" c="dimmed" span>get_space, list_spaces, create_space, update_space</Text></List.Item>
|
||||
<List.Item><Text size="sm" c="dimmed" span>get_comments, create_comment, update_comment</Text></List.Item>
|
||||
<List.Item><Text size="sm" c="dimmed" span>search_attachments, list_workspace_members, get_current_user</Text></List.Item>
|
||||
<List.Item>
|
||||
<Text size="sm" c="dimmed" span>
|
||||
search_pages, get_page, create_page, update_page
|
||||
</Text>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text size="sm" c="dimmed" span>
|
||||
list_pages, list_child_pages, duplicate_page
|
||||
</Text>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text size="sm" c="dimmed" span>
|
||||
copy_page_to_space, move_page, move_page_to_space
|
||||
</Text>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text size="sm" c="dimmed" span>
|
||||
get_space, list_spaces, create_space, update_space
|
||||
</Text>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text size="sm" c="dimmed" span>
|
||||
get_comments, create_comment, update_comment
|
||||
</Text>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Text size="sm" c="dimmed" span>
|
||||
search_attachments, list_workspace_members, get_current_user
|
||||
</Text>
|
||||
</List.Item>
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,14 +9,17 @@ import EnableGenerativeAi from "@/ee/ai/components/enable-generative-ai.tsx";
|
||||
import McpSettings from "@/ee/ai/components/mcp-settings.tsx";
|
||||
import { Alert, Stack, Tabs } from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { useIsCloudEE } from "@/hooks/use-is-cloud-ee.tsx";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
export default function AiSettings() {
|
||||
const { t } = useTranslation();
|
||||
const { isAdmin } = useUserRole();
|
||||
const hasAccess = useIsCloudEE();
|
||||
const hasAccess = useHasFeature(Feature.AI);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -55,7 +58,7 @@ export default function AiSettings() {
|
||||
{!hasAccess && (
|
||||
<Alert
|
||||
icon={<IconInfoCircle />}
|
||||
title={t("Enterprise feature")}
|
||||
title={upgradeLabel}
|
||||
color="blue"
|
||||
mb="lg"
|
||||
>
|
||||
|
||||
@@ -5,12 +5,14 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import useEnterpriseAccess from "@/ee/hooks/use-enterprise-access.tsx";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import {
|
||||
ResponsiveSettingsRow,
|
||||
ResponsiveSettingsContent,
|
||||
ResponsiveSettingsControl,
|
||||
} from "@/components/ui/responsive-settings-row";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
|
||||
|
||||
export default function RestrictApiToAdmins() {
|
||||
const { t } = useTranslation();
|
||||
@@ -18,7 +20,8 @@ export default function RestrictApiToAdmins() {
|
||||
const [checked, setChecked] = useState(
|
||||
workspace?.settings?.api?.restrictToAdmins === true,
|
||||
);
|
||||
const hasAccess = useEnterpriseAccess();
|
||||
const hasAccess = useHasFeature(Feature.API_KEYS);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
@@ -51,7 +54,7 @@ export default function RestrictApiToAdmins() {
|
||||
|
||||
<ResponsiveSettingsControl>
|
||||
<Tooltip
|
||||
label={t("Requires an enterprise license")}
|
||||
label={upgradeLabel}
|
||||
disabled={hasAccess}
|
||||
refProp="rootRef"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||
import { Anchor, Alert, Button, Group, Space, Text } from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import SettingsTitle from "@/components/settings/settings-title";
|
||||
import { getAppName, getAppUrl } from "@/lib/config";
|
||||
import { ApiKeyTable } from "@/ee/api-key/components/api-key-table";
|
||||
@@ -58,11 +58,12 @@ export default function UserApiKeys() {
|
||||
<SettingsTitle title={t("API keys")} />
|
||||
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
{t("View the")}{" "}
|
||||
<Anchor href="https://docmost.com/api-docs" target="_blank" size="sm">
|
||||
{t("API documentation")}
|
||||
</Anchor>{" "}
|
||||
{t("for usage details.")}
|
||||
<Trans
|
||||
i18nKey="View the <anchor>API documentation</anchor> for usage details."
|
||||
components={{
|
||||
anchor: <Anchor href="https://docmost.com/api-docs" target="_blank" size="sm" />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
{mcpEnabled && canCreate && (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { Anchor, Button, Divider, Group, Space, Text } from "@mantine/core";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import SettingsTitle from "@/components/settings/settings-title";
|
||||
import { getAppName } from "@/lib/config";
|
||||
import { ApiKeyTable } from "@/ee/api-key/components/api-key-table";
|
||||
@@ -56,12 +56,12 @@ export default function WorkspaceApiKeys() {
|
||||
<SettingsTitle title={t("API management")} />
|
||||
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
{t("Manage API keys for all users in the workspace.")}{" "}
|
||||
{t("View the")}{" "}
|
||||
<Anchor href="https://docmost.com/api-docs" target="_blank" size="sm">
|
||||
{t("API documentation")}
|
||||
</Anchor>{" "}
|
||||
{t("for usage details.")}
|
||||
<Trans
|
||||
i18nKey="Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details."
|
||||
components={{
|
||||
anchor: <Anchor href="https://docmost.com/api-docs" target="_blank" size="sm" />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<RestrictApiToAdmins />
|
||||
|
||||
@@ -6,7 +6,6 @@ import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
import { buildSsoLoginUrl } from "@/ee/security/sso.utils.ts";
|
||||
import { SSO_PROVIDER } from "@/ee/security/contants.ts";
|
||||
import { GoogleIcon } from "@/components/icons/google-icon.tsx";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import { LdapLoginModal } from "@/ee/components/ldap-login-modal.tsx";
|
||||
|
||||
export default function SsoLogin() {
|
||||
@@ -57,7 +56,7 @@ export default function SsoLogin() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{(isCloud() || data.hasLicenseKey) && (
|
||||
{data.authProviders.length > 0 && (
|
||||
<>
|
||||
<Stack align="stretch" justify="center" gap="sm">
|
||||
{data.authProviders.map((provider) => (
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import type { Entitlements } from "./entitlement.types";
|
||||
|
||||
export const entitlementAtom = atomWithStorage<Entitlements | null>(
|
||||
"entitlements",
|
||||
null,
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import api from "@/lib/api-client";
|
||||
import { Entitlements } from "./entitlement.types";
|
||||
|
||||
export async function getEntitlements(): Promise<Entitlements> {
|
||||
const req = await api.post<Entitlements>("/workspace/entitlements");
|
||||
return req.data as Entitlements;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export type Tier = "free" | "standard" | "business" | "enterprise";
|
||||
|
||||
export type Entitlements = {
|
||||
cloud: boolean;
|
||||
tier: Tier;
|
||||
features: string[];
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||
import { getEntitlements } from "./entitlement-service";
|
||||
import { Entitlements } from "./entitlement.types";
|
||||
|
||||
export function useEntitlements(): UseQueryResult<Entitlements> {
|
||||
return useQuery({
|
||||
queryKey: ["entitlements"],
|
||||
queryFn: getEntitlements,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export const Feature = {
|
||||
SSO_CUSTOM: 'sso:custom',
|
||||
SSO_GOOGLE: 'sso:google',
|
||||
MFA: 'mfa',
|
||||
API_KEYS: 'api:keys',
|
||||
COMMENT_RESOLUTION: 'comment:resolution',
|
||||
PAGE_PERMISSIONS: 'page:permissions',
|
||||
AI: 'ai',
|
||||
CONFLUENCE_IMPORT: 'import:confluence',
|
||||
DOCX_IMPORT: 'import:docx',
|
||||
ATTACHMENT_INDEXING: 'attachment:indexing',
|
||||
SECURITY_SETTINGS: 'security:settings',
|
||||
MCP: 'mcp',
|
||||
SCIM: 'scim',
|
||||
PAGE_VERIFICATION: 'page:verification',
|
||||
AUDIT_LOGS: 'audit:logs',
|
||||
RETENTION: 'retention',
|
||||
SHARING_CONTROLS: 'sharing:controls',
|
||||
} as const;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { isCloud } from "@/lib/config";
|
||||
import useLicense from "@/ee/hooks/use-license";
|
||||
import usePlan from "@/ee/hooks/use-plan";
|
||||
|
||||
const useEnterpriseAccess = () => {
|
||||
const { hasLicenseKey } = useLicense();
|
||||
const { isBusiness } = usePlan();
|
||||
|
||||
return (isCloud() && isBusiness) || (!isCloud() && hasLicenseKey);
|
||||
};
|
||||
|
||||
export default useEnterpriseAccess;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
|
||||
|
||||
export const useHasFeature = (feature: string): boolean => {
|
||||
const [entitlements] = useAtom(entitlementAtom);
|
||||
return entitlements?.features?.includes(feature) ?? false;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
|
||||
export const useLicense = () => {
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
return { hasLicenseKey: currentUser?.workspace?.hasLicenseKey };
|
||||
};
|
||||
|
||||
export default useLicense;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
|
||||
import { isCloud } from "@/lib/config";
|
||||
|
||||
export function useUpgradeLabel(): string {
|
||||
const { t } = useTranslation();
|
||||
const [entitlements] = useAtom(entitlementAtom);
|
||||
|
||||
if (!isCloud()) {
|
||||
return entitlements != null && entitlements.tier !== "free"
|
||||
? t("Upgrade your license tier.")
|
||||
: t("Available with a paid license");
|
||||
}
|
||||
return t("Upgrade your plan");
|
||||
}
|
||||
@@ -7,21 +7,22 @@ import { useTranslation } from "react-i18next";
|
||||
import { useActivateMutation } from "@/ee/licence/queries/license-query.ts";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
|
||||
import RemoveLicense from "@/ee/licence/components/remove-license.tsx";
|
||||
|
||||
export default function ActivateLicense() {
|
||||
const { t } = useTranslation();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
const [entitlements] = useAtom(entitlementAtom);
|
||||
const hasLicense = entitlements != null && entitlements.tier !== "free";
|
||||
|
||||
return (
|
||||
<Group justify="flex-end" wrap="nowrap" mb="sm">
|
||||
<Button onClick={open}>
|
||||
{workspace?.hasLicenseKey ? t("Update license") : t("Add license")}
|
||||
{hasLicense ? t("Update license") : t("Add license")}
|
||||
</Button>
|
||||
|
||||
{workspace?.hasLicenseKey && <RemoveLicense />}
|
||||
{hasLicense && <RemoveLicense />}
|
||||
|
||||
<Modal
|
||||
size="550"
|
||||
@@ -59,7 +60,7 @@ export function ActivateLicenseForm({ onClose }: ActivateLicenseFormProps) {
|
||||
async function handleSubmit(data: { licenseKey: string }) {
|
||||
await activateLicenseMutation.mutateAsync(data.licenseKey);
|
||||
form.reset();
|
||||
onClose();
|
||||
onClose?.();
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -31,7 +31,8 @@ export default function LicenseDetails() {
|
||||
<Table.Tr>
|
||||
<Table.Th w={160}>Edition</Table.Th>
|
||||
<Table.Td>
|
||||
Enterprise {license.trial && <Badge color="green">Trial</Badge>}
|
||||
{license.licenseType === "business" ? "Business" : "Enterprise"}{" "}
|
||||
{license.trial && <Badge color="green">Trial</Badge>}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@ import ActivateLicenseForm from "@/ee/licence/components/activate-license-modal.
|
||||
import InstallationDetails from "@/ee/licence/components/installation-details.tsx";
|
||||
import OssDetails from "@/ee/licence/components/oss-details.tsx";
|
||||
import { useAtom } from "jotai/index";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
|
||||
|
||||
export default function License() {
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
const [entitlements] = useAtom(entitlementAtom);
|
||||
const hasLicense = entitlements != null && entitlements.tier !== "free";
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
if (!isAdmin) {
|
||||
@@ -29,7 +30,7 @@ export default function License() {
|
||||
|
||||
<InstallationDetails />
|
||||
|
||||
{workspace?.hasLicenseKey ? <LicenseDetails /> : <OssDetails />}
|
||||
{hasLicense ? <LicenseDetails /> : <OssDetails />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export function useActivateMutation() {
|
||||
queryKey: ["license"],
|
||||
});
|
||||
queryClient.refetchQueries({ queryKey: ["currentUser"] });
|
||||
queryClient.refetchQueries({ queryKey: ["entitlements"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error["response"]?.data?.message;
|
||||
@@ -47,6 +48,7 @@ export function useRemoveLicenseMutation() {
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["license"] });
|
||||
queryClient.refetchQueries({ queryKey: ["currentUser"] });
|
||||
queryClient.refetchQueries({ queryKey: ["entitlements"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
export type LicenseType = 'business' | 'enterprise';
|
||||
|
||||
export interface ILicenseInfo {
|
||||
id: string;
|
||||
customerName: string;
|
||||
seatCount: number;
|
||||
licenseType: LicenseType;
|
||||
issuedAt: Date;
|
||||
expiresAt: Date;
|
||||
trial: boolean;
|
||||
|
||||
@@ -7,8 +7,9 @@ import { getMfaStatus } from "@/ee/mfa";
|
||||
import { MfaSetupModal } from "@/ee/mfa";
|
||||
import { MfaDisableModal } from "@/ee/mfa";
|
||||
import { MfaBackupCodesModal } from "@/ee/mfa";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import useLicense from "@/ee/hooks/use-license.tsx";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
import { ResponsiveSettingsRow, ResponsiveSettingsContent, ResponsiveSettingsControl } from "@/components/ui/responsive-settings-row";
|
||||
|
||||
export function MfaSettings() {
|
||||
@@ -17,7 +18,8 @@ export function MfaSettings() {
|
||||
const [setupModalOpen, setSetupModalOpen] = useState(false);
|
||||
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
||||
const [backupCodesModalOpen, setBackupCodesModalOpen] = useState(false);
|
||||
const { hasLicenseKey } = useLicense();
|
||||
const canUseMfa = useHasFeature(Feature.MFA);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const { data: mfaStatus, isLoading } = useQuery({
|
||||
queryKey: ["mfa-status"],
|
||||
@@ -28,8 +30,6 @@ export function MfaSettings() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canUseMfa = isCloud() || hasLicenseKey;
|
||||
|
||||
// Check if MFA is truly enabled
|
||||
const isMfaEnabled = mfaStatus?.isEnabled === true;
|
||||
|
||||
@@ -69,7 +69,7 @@ export function MfaSettings() {
|
||||
<ResponsiveSettingsControl>
|
||||
{!isMfaEnabled ? (
|
||||
<Tooltip
|
||||
label={t("Available in enterprise edition")}
|
||||
label={upgradeLabel}
|
||||
disabled={canUseMfa}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -19,7 +19,8 @@ import { usePageRestrictionInfoQuery } from "@/ee/page-permission/queries/page-p
|
||||
import { PagePermissionTab } from "@/ee/page-permission";
|
||||
import { PublishTab } from "./publish-tab";
|
||||
import { useShareForPageQuery } from "@/features/share/queries/share-query";
|
||||
import { useIsCloudEE } from "@/hooks/use-is-cloud-ee";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
|
||||
import { useSpaceQuery } from "@/features/space/queries/space-query";
|
||||
@@ -33,9 +34,9 @@ export function PageShareModal({ readOnly }: PageShareModalProps) {
|
||||
const { pageSlug, spaceSlug } = useParams();
|
||||
const pageSlugId = extractPageSlugId(pageSlug);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const isCloudEE = useIsCloudEE();
|
||||
const hasPagePermissions = useHasFeature(Feature.PAGE_PERMISSIONS);
|
||||
const [activeTab, setActiveTab] = useState<string | null>(
|
||||
isCloudEE ? "access" : "publish",
|
||||
hasPagePermissions ? "access" : "publish",
|
||||
);
|
||||
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
@@ -51,7 +52,7 @@ export function PageShareModal({ readOnly }: PageShareModalProps) {
|
||||
const isPubliclyShared = !!share;
|
||||
|
||||
const { data: restrictionInfo, isLoading: restrictionLoading } =
|
||||
usePageRestrictionInfoQuery(opened && isCloudEE ? pageId : undefined);
|
||||
usePageRestrictionInfoQuery(opened && hasPagePermissions ? pageId : undefined);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -92,7 +93,7 @@ export function PageShareModal({ readOnly }: PageShareModalProps) {
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="access">
|
||||
{!isCloudEE ? (
|
||||
{!hasPagePermissions ? (
|
||||
<Stack align="center" py="md">
|
||||
<IconLock size={20} stroke={1.5} />
|
||||
<Text size="sm" ta="center" fw={500}>
|
||||
|
||||
@@ -6,21 +6,23 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import useEnterpriseAccess from "@/ee/hooks/use-enterprise-access.tsx";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
|
||||
|
||||
export default function DisablePublicSharing() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Disable public sharing")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("Prevent members from sharing pages publicly.")}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="md">{t("Disable public sharing")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("Prevent members from sharing pages publicly.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<DisablePublicSharingToggle />
|
||||
<DisablePublicSharingToggle />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -31,7 +33,8 @@ function DisablePublicSharingToggle() {
|
||||
const [checked, setChecked] = useState(
|
||||
workspace?.settings?.sharing?.disabled === true,
|
||||
);
|
||||
const hasAccess = useEnterpriseAccess();
|
||||
const hasSharingControls = useHasFeature(Feature.SHARING_CONTROLS);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const applyChange = async (value: boolean) => {
|
||||
try {
|
||||
@@ -72,15 +75,11 @@ function DisablePublicSharingToggle() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={t("Requires an enterprise license")}
|
||||
disabled={hasAccess}
|
||||
refProp="rootRef"
|
||||
>
|
||||
<Tooltip label={upgradeLabel} disabled={hasSharingControls} refProp="rootRef">
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
disabled={!hasSharingControls}
|
||||
aria-label={t("Toggle public sharing")}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { Group, Text, Switch, MantineSize, Title } from "@mantine/core";
|
||||
import {
|
||||
Group,
|
||||
Text,
|
||||
Switch,
|
||||
MantineSize,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature.ts";
|
||||
import { Feature } from "@/ee/features.ts";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
|
||||
|
||||
export default function EnforceMfa() {
|
||||
const { t } = useTranslation();
|
||||
@@ -33,6 +43,8 @@ export function EnforceMfaToggle({ size, label }: EnforceMfaToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(workspace?.enforceMfa);
|
||||
const hasAccess = useHasFeature(Feature.MFA);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
@@ -49,13 +61,16 @@ export function EnforceMfaToggle({ size, label }: EnforceMfaToggleProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
size={size}
|
||||
label={label}
|
||||
labelPosition="left"
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
aria-label={t("Toggle MFA enforcement")}
|
||||
/>
|
||||
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
|
||||
<Switch
|
||||
size={size}
|
||||
label={label}
|
||||
labelPosition="left"
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
aria-label={t("Toggle MFA enforcement")}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Group, Text, Switch, MantineSize } from "@mantine/core";
|
||||
import { Group, Text, Switch, MantineSize, Tooltip } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature.ts";
|
||||
import { Feature } from "@/ee/features.ts";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
|
||||
|
||||
export default function EnforceSso() {
|
||||
const { t } = useTranslation();
|
||||
@@ -33,6 +36,8 @@ export function EnforceSsoToggle({ size, label }: EnforceSsoToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(workspace?.enforceSso);
|
||||
const hasAccess = useHasFeature(Feature.SSO_CUSTOM);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
@@ -49,13 +54,16 @@ export function EnforceSsoToggle({ size, label }: EnforceSsoToggleProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
size={size}
|
||||
label={label}
|
||||
labelPosition="left"
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
aria-label={t("Toggle sso enforcement")}
|
||||
/>
|
||||
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
|
||||
<Switch
|
||||
size={size}
|
||||
label={label}
|
||||
labelPosition="left"
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={!hasAccess}
|
||||
aria-label={t("Toggle sso enforcement")}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ISpace } from "@/features/space/types/space.types.ts";
|
||||
import { useUpdateSpaceMutation } from "@/features/space/queries/space-query.ts";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature.ts";
|
||||
import { Feature } from "@/ee/features.ts";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
|
||||
|
||||
type SpacePublicSharingToggleProps = {
|
||||
space: ISpace;
|
||||
@@ -17,6 +20,9 @@ export default function SpacePublicSharingToggle({
|
||||
const { t } = useTranslation();
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
const workspaceDisabled = workspace?.settings?.sharing?.disabled === true;
|
||||
const hasSharingControls = useHasFeature(Feature.SHARING_CONTROLS);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
const isDisabled = !hasSharingControls || workspaceDisabled;
|
||||
const [checked, setChecked] = useState(
|
||||
space.settings?.sharing?.disabled === true,
|
||||
);
|
||||
@@ -68,14 +74,14 @@ export default function SpacePublicSharingToggle({
|
||||
</Text>
|
||||
</div>
|
||||
<Tooltip
|
||||
label={t("Public sharing is disabled at the workspace level")}
|
||||
disabled={!workspaceDisabled}
|
||||
label={!hasSharingControls ? upgradeLabel : t("Public sharing is disabled at the workspace level")}
|
||||
disabled={!isDisabled}
|
||||
refProp="rootRef"
|
||||
>
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
disabled={workspaceDisabled}
|
||||
disabled={isDisabled}
|
||||
aria-label={t("Toggle space public sharing")}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -12,13 +12,18 @@ import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import useEnterpriseAccess from "@/ee/hooks/use-enterprise-access.tsx";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
|
||||
|
||||
type RetentionUnit = "days" | "months" | "years";
|
||||
|
||||
const DEFAULT_RETENTION_DAYS = 30;
|
||||
|
||||
function daysToRetention(days: number): { amount: number; unit: RetentionUnit } {
|
||||
function daysToRetention(days: number): {
|
||||
amount: number;
|
||||
unit: RetentionUnit;
|
||||
} {
|
||||
if (days >= 365 && days % 365 === 0) {
|
||||
return { amount: days / 365, unit: "years" };
|
||||
}
|
||||
@@ -36,14 +41,19 @@ function retentionToDays(amount: number, unit: RetentionUnit): number {
|
||||
|
||||
export default function TrashRetention() {
|
||||
const { t } = useTranslation();
|
||||
const hasAccess = useEnterpriseAccess();
|
||||
const hasRetention = useHasFeature(Feature.RETENTION);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
|
||||
const currentDays = workspace?.trashRetentionDays ?? DEFAULT_RETENTION_DAYS;
|
||||
const parsed = daysToRetention(currentDays);
|
||||
|
||||
const [retentionAmount, setRetentionAmount] = useState<number | string>(parsed.amount);
|
||||
const [retentionUnit, setRetentionUnit] = useState<RetentionUnit>(parsed.unit);
|
||||
const [retentionAmount, setRetentionAmount] = useState<number | string>(
|
||||
parsed.amount,
|
||||
);
|
||||
const [retentionUnit, setRetentionUnit] = useState<RetentionUnit>(
|
||||
parsed.unit,
|
||||
);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -63,14 +73,17 @@ export default function TrashRetention() {
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace({ trashRetentionDays: days });
|
||||
const updatedWorkspace = await updateWorkspace({
|
||||
trashRetentionDays: days,
|
||||
});
|
||||
setWorkspace(updatedWorkspace);
|
||||
notifications.show({
|
||||
message: t("Trash retention updated"),
|
||||
});
|
||||
} catch (err: any) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message || t("Failed to update trash retention"),
|
||||
message:
|
||||
err?.response?.data?.message || t("Failed to update trash retention"),
|
||||
color: "red",
|
||||
});
|
||||
const { amount, unit } = daysToRetention(currentDays);
|
||||
@@ -81,10 +94,11 @@ export default function TrashRetention() {
|
||||
}
|
||||
};
|
||||
|
||||
const isDirty = retentionToDays(
|
||||
typeof retentionAmount === "number" ? retentionAmount : 1,
|
||||
retentionUnit,
|
||||
) !== currentDays;
|
||||
const isDirty =
|
||||
retentionToDays(
|
||||
typeof retentionAmount === "number" ? retentionAmount : 1,
|
||||
retentionUnit,
|
||||
) !== currentDays;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -93,10 +107,7 @@ export default function TrashRetention() {
|
||||
{t("Pages in trash will be permanently deleted after this period.")}
|
||||
</Text>
|
||||
|
||||
<Tooltip
|
||||
label={t("Requires an enterprise license")}
|
||||
disabled={hasAccess}
|
||||
>
|
||||
<Tooltip label={upgradeLabel} disabled={hasRetention}>
|
||||
<Group gap="xs" wrap="nowrap" maw={320}>
|
||||
<NumberInput
|
||||
value={retentionAmount}
|
||||
@@ -105,7 +116,7 @@ export default function TrashRetention() {
|
||||
hideControls
|
||||
size="sm"
|
||||
w={60}
|
||||
disabled={!hasAccess}
|
||||
disabled={!hasRetention}
|
||||
/>
|
||||
<Select
|
||||
data={[
|
||||
@@ -121,13 +132,13 @@ export default function TrashRetention() {
|
||||
}}
|
||||
size="sm"
|
||||
style={{ flex: 1 }}
|
||||
disabled={!hasAccess}
|
||||
disabled={!hasRetention}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={!hasAccess || !isDirty}
|
||||
disabled={!hasRetention || !isDirty}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
|
||||
@@ -12,14 +12,15 @@ import { useTranslation } from "react-i18next";
|
||||
import EnforceMfa from "@/ee/security/components/enforce-mfa.tsx";
|
||||
import DisablePublicSharing from "@/ee/security/components/disable-public-sharing.tsx";
|
||||
import TrashRetention from "@/ee/security/components/trash-retention.tsx";
|
||||
import useEnterpriseAccess from "@/ee/hooks/use-enterprise-access.tsx";
|
||||
import { useIsCloudEE } from "@/hooks/use-is-cloud-ee.tsx";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
|
||||
export default function Security() {
|
||||
const { t } = useTranslation();
|
||||
const { isAdmin } = useUserRole();
|
||||
const hasEnterpriseAccess = useEnterpriseAccess();
|
||||
const isCloudEE = useIsCloudEE();
|
||||
const hasCustomSso = useHasFeature(Feature.SSO_CUSTOM);
|
||||
const hasRetention = useHasFeature(Feature.RETENTION);
|
||||
const hasSharingControls = useHasFeature(Feature.SHARING_CONTROLS);
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
@@ -36,39 +37,27 @@ export default function Security() {
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
{(!isCloud() || hasEnterpriseAccess) && (
|
||||
<>
|
||||
<DisablePublicSharing />
|
||||
<Divider my="lg" />
|
||||
</>
|
||||
)}
|
||||
<DisablePublicSharing />
|
||||
<Divider my="lg" />
|
||||
|
||||
{!isCloud() && (
|
||||
<>
|
||||
<TrashRetention />
|
||||
<Divider my="lg" />
|
||||
</>
|
||||
)}
|
||||
<TrashRetention />
|
||||
<Divider my="lg" />
|
||||
|
||||
<Title order={4} my="lg">
|
||||
Single sign-on (SSO)
|
||||
</Title>
|
||||
|
||||
{hasEnterpriseAccess && (
|
||||
<>
|
||||
<EnforceSso />
|
||||
<Divider my="lg" />
|
||||
</>
|
||||
)}
|
||||
<EnforceSso />
|
||||
<Divider my="lg" />
|
||||
|
||||
{isCloudEE && (
|
||||
{(isCloud() || hasCustomSso) && (
|
||||
<>
|
||||
<AllowedDomains />
|
||||
<Divider my="lg" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasEnterpriseAccess && (
|
||||
{hasCustomSso && (
|
||||
<>
|
||||
<CreateSsoProvider />
|
||||
<Divider size={0} my="lg" />
|
||||
|
||||
Reference in New Issue
Block a user