Prepare EE to merge with MIT

This commit is contained in:
Weves
2024-02-15 14:45:00 -08:00
committed by Chris Weaver
parent f0b2b57d81
commit 1ee8ee9e8b
46 changed files with 352 additions and 322 deletions

View File

@@ -1,11 +1,11 @@
FROM node:20-alpine AS base
LABEL com.danswer.maintainer="founders@danswer.ai"
LABEL com.danswer.description="This image is the Enterprise Edition (Paid Edition) \
frontend/webserver of Danswer. If you do not have a contract or agreement with DanswerAI, you are \
not permitted to use this container outside of personal development or testing purposes. Please \
reach out to founders@danswer.ai for more information. You can access the MIT version of Danswer \
at https://github.com/danswer-ai/danswer"
LABEL com.danswer.description="This image is the web/frontend container of Danswer which \
contains code for both the Community and Enterprise editions of Danswer. If you do not \
have a contract or agreement with DanswerAI, you are not permitted to use the Enterprise \
Edition features outside of personal development or testing purposes. Please reach out to \
founders@danswer.ai for more information. Please visit https://github.com/danswer-ai/danswer"
# Default DANSWER_VERSION, typically overriden during builds by GitHub Actions.
ARG DANSWER_VERSION=0.3-dev

View File

@@ -9,64 +9,17 @@ const nextConfig = {
output: "standalone",
swcMinify: true,
rewrites: async () => {
const eeRedirects =
process.env.NEXT_PUBLIC_ENABLE_PAID_EE_FEATURES?.toLowerCase() === "true"
? [
// user group pages
{
source: "/admin/groups",
destination: "/ee/admin/groups",
},
{
source: "/admin/groups/:path*",
destination: "/ee/admin/groups/:path*",
},
{
source: "/admin/api-key",
destination: "/ee/admin/api-key",
},
// analytics / audit log pages
{
source: "/admin/performance/usage",
destination: "/ee/admin/performance/usage",
},
{
source: "/admin/performance/query-history",
destination: "/ee/admin/performance/query-history",
},
{
source: "/admin/performance/query-history/:path*",
destination: "/ee/admin/performance/query-history/:path*",
},
// whitelabeling
{
source: "/admin/whitelabeling",
destination: "/ee/admin/whitelabeling",
},
// custom analytics/tracking
{
source: "/admin/performance/custom-analytics",
destination: "/ee/admin/performance/custom-analytics",
},
// token rate limits
{
source: "/admin/token-rate-limits",
destination: "/ee/admin/token-rate-limits",
},
]
: [];
// In production, something else (nginx in the one box setup) should take
// care of this rewrite. TODO (chris): better support setups where
// web_server and api_server are on different machines.
if (process.env.NODE_ENV === "production") return eeRedirects;
if (process.env.NODE_ENV === "production") return [];
return [
{
source: "/api/:path*",
destination: "http://127.0.0.1:8080/:path*", // Proxy to Backend
},
].concat(eeRedirects);
];
},
redirects: async () => {
// In production, something else (nginx in the one box setup) should take

View File

@@ -25,7 +25,6 @@ import {
} from "@/components/admin/connectors/Field";
import { HidableSection } from "./HidableSection";
import { FiPlus, FiX } from "react-icons/fi";
import { EE_ENABLED } from "@/lib/constants";
import { useUserGroups } from "@/lib/hooks";
import { Bubble } from "@/components/Bubble";
import { GroupsIcon } from "@/components/icons/icons";
@@ -37,6 +36,8 @@ import { ToolSnapshot } from "@/lib/tools/interfaces";
import { checkUserIsNoAuthUser } from "@/lib/user";
import { addAssistantToList } from "@/lib/assistants/updateAssistantPreferences";
import { checkLLMSupportsImageInput } from "@/lib/llm/utils";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
function findSearchTool(tools: ToolSnapshot[]) {
return tools.find((tool) => tool.in_code_tool_id === "SearchTool");
@@ -80,6 +81,8 @@ export function AssistantEditor({
const router = useRouter();
const { popup, setPopup } = usePopup();
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
// EE only
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
@@ -868,7 +871,7 @@ export function AssistantEditor({
<Divider />
{EE_ENABLED &&
{isPaidEnterpriseFeaturesEnabled &&
userGroups &&
(!user || user.role === "admin") && (
<>

View File

@@ -25,13 +25,15 @@ import { getNameFromPath } from "@/lib/fileUtils";
import { Button, Card, Divider, Text } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
import IsPublicField from "@/components/admin/connectors/IsPublicField";
import { EE_ENABLED } from "@/lib/constants";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
const Main = () => {
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [filesAreUploading, setFilesAreUploading] = useState<boolean>(false);
const { popup, setPopup } = usePopup();
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
const { mutate } = useSWRConfig();
const {
@@ -90,13 +92,13 @@ const Main = () => {
initialValues={{
name: "",
selectedFiles: [],
is_public: EE_ENABLED ? false : undefined,
is_public: isPaidEnterpriseFeaturesEnabled ? false : undefined,
}}
validationSchema={Yup.object().shape({
name: Yup.string().required(
"Please enter a descriptive name for the files"
),
...(EE_ENABLED && {
...(isPaidEnterpriseFeaturesEnabled && {
is_public: Yup.boolean().required(),
}),
})}
@@ -226,7 +228,7 @@ const Main = () => {
setSelectedFiles={setSelectedFiles}
/>
{EE_ENABLED && (
{isPaidEnterpriseFeaturesEnabled && (
<>
<Divider />
<IsPublicField />

View File

@@ -1,3 +1,5 @@
"use client";
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
import * as Yup from "yup";
import { PopupSpec } from "@/components/admin/connectors/Popup";
@@ -9,8 +11,8 @@ import {
} from "@/components/admin/connectors/Field";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { Button, Divider, Text } from "@tremor/react";
import { EE_ENABLED } from "@/lib/constants";
import { FiUsers } from "react-icons/fi";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
interface SetCreationPopupProps {
ccPairs: ConnectorIndexingStatus<any, any>[];
@@ -27,6 +29,8 @@ export const DocumentSetCreationForm = ({
setPopup,
existingDocumentSet,
}: SetCreationPopupProps) => {
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
const isUpdate = existingDocumentSet !== undefined;
return (
@@ -167,46 +171,48 @@ export const DocumentSetCreationForm = ({
)}
/>
{EE_ENABLED && userGroups && userGroups.length > 0 && (
<div>
<Divider />
{isPaidEnterpriseFeaturesEnabled &&
userGroups &&
userGroups.length > 0 && (
<div>
<Divider />
<BooleanFormField
name="is_public"
label="Is Public?"
subtext={
<BooleanFormField
name="is_public"
label="Is Public?"
subtext={
<>
If the document set is public, then it will be visible
to <b>all users</b>. If it is not public, then only
users in the specified groups will be able to see it.
</>
}
/>
<Divider />
<h2 className="mb-1 font-medium text-base">
Groups with Access
</h2>
{!values.is_public ? (
<>
If the document set is public, then it will be visible to{" "}
<b>all users</b>. If it is not public, then only users in
the specified groups will be able to see it.
</>
}
/>
<Divider />
<h2 className="mb-1 font-medium text-base">
Groups with Access
</h2>
{!values.is_public ? (
<>
<Text className="mb-3">
If any groups are specified, then this Document Set will
only be visible to the specified groups. If no groups are
specified, then the Document Set will be visible to all
users.
</Text>
<FieldArray
name="groups"
render={(arrayHelpers: ArrayHelpers) => (
<div className="flex gap-2 flex-wrap">
{userGroups.map((userGroup) => {
const ind = values.groups.indexOf(userGroup.id);
let isSelected = ind !== -1;
return (
<div
key={userGroup.id}
className={
`
<Text className="mb-3">
If any groups are specified, then this Document Set will
only be visible to the specified groups. If no groups
are specified, then the Document Set will be visible to
all users.
</Text>
<FieldArray
name="groups"
render={(arrayHelpers: ArrayHelpers) => (
<div className="flex gap-2 flex-wrap">
{userGroups.map((userGroup) => {
const ind = values.groups.indexOf(userGroup.id);
let isSelected = ind !== -1;
return (
<div
key={userGroup.id}
className={
`
px-3
py-1
rounded-lg
@@ -215,38 +221,38 @@ export const DocumentSetCreationForm = ({
w-fit
flex
cursor-pointer ` +
(isSelected
? " bg-background-strong"
: " hover:bg-hover")
}
onClick={() => {
if (isSelected) {
arrayHelpers.remove(ind);
} else {
arrayHelpers.push(userGroup.id);
(isSelected
? " bg-background-strong"
: " hover:bg-hover")
}
}}
>
<div className="my-auto flex">
<FiUsers className="my-auto mr-2" />{" "}
{userGroup.name}
onClick={() => {
if (isSelected) {
arrayHelpers.remove(ind);
} else {
arrayHelpers.push(userGroup.id);
}
}}
>
<div className="my-auto flex">
<FiUsers className="my-auto mr-2" />{" "}
{userGroup.name}
</div>
</div>
</div>
);
})}
</div>
)}
/>
</>
) : (
<Text>
This Document Set is public, so this does not apply. If you
want to control which user groups see this Document Set,
mark it as non-public!
</Text>
)}
</div>
)}
);
})}
</div>
)}
/>
</>
) : (
<Text>
This Document Set is public, so this does not apply. If
you want to control which user groups see this Document
Set, mark it as non-public!
</Text>
)}
</div>
)}
<div className="flex mt-6">
<Button
type="submit"

View File

@@ -1,4 +1,3 @@
import { EE_ENABLED } from "@/lib/constants";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { DocumentSet } from "@/lib/types";
import useSWR, { mutate } from "swr";

View File

@@ -1,6 +1,5 @@
import { NotebookIcon } from "@/components/icons/icons";
import { getWebVersion, getBackendVersion } from "@/lib/version";
import { EE_ENABLED } from "@/lib/constants";
const Page = async () => {
let web_version: string | null = null;
@@ -22,9 +21,6 @@ const Page = async () => {
</div>
<div>
<p className="font-bold text-lg my-auto mb-2">
{EE_ENABLED ? "Danswer Enterprise Edition" : "Danswer MIT"}
</p>
<div className="flex mb-2">
<p className="my-auto mr-1">Backend Version: </p>
<p className="text-base my-auto text-slate-400 italic">

View File

@@ -22,7 +22,7 @@ import { GenericTokenRateLimitTable } from "./TokenRateLimitTables";
import { mutate } from "swr";
import { usePopup } from "@/components/admin/connectors/Popup";
import { CreateRateLimitModal } from "./CreateRateLimitModal";
import { EE_ENABLED } from "@/lib/constants";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
const BASE_URL = "/api/admin/token-rate-limits";
const GLOBAL_TOKEN_FETCH_URL = `${BASE_URL}/global`;
@@ -69,6 +69,8 @@ function Main() {
const [modalIsOpen, setModalIsOpen] = useState(false);
const { popup, setPopup } = usePopup();
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
const updateTable = (target_scope: Scope) => {
if (target_scope === Scope.GLOBAL) {
mutate(GLOBAL_TOKEN_FETCH_URL);
@@ -120,7 +122,7 @@ function Main() {
token spend.
</Text>
</li>
{EE_ENABLED && (
{isPaidEnterpriseFeaturesEnabled && (
<>
<li>
<Text>
@@ -150,7 +152,7 @@ function Main() {
Create a Token Rate Limit
</Button>
{EE_ENABLED && (
{isPaidEnterpriseFeaturesEnabled && (
<TabGroup className="mt-6" index={tabIndex} onIndexChange={setTabIndex}>
<TabList variant="line">
<Tab icon={FiGlobe}>Global</Tab>
@@ -191,7 +193,7 @@ function Main() {
</TabGroup>
)}
{!EE_ENABLED && (
{!isPaidEnterpriseFeaturesEnabled && (
<div className="mt-6">
<GenericTokenRateLimitTable
fetchUrl={GLOBAL_TOKEN_FETCH_URL}
@@ -206,7 +208,9 @@ function Main() {
setIsOpen={() => setModalIsOpen(false)}
setPopup={setPopup}
onSubmit={handleSubmit}
forSpecificScope={EE_ENABLED ? undefined : Scope.GLOBAL}
forSpecificScope={
isPaidEnterpriseFeaturesEnabled ? undefined : Scope.GLOBAL
}
/>
</div>
);

View File

@@ -1,5 +1,5 @@
The DanswerAI Enterprise license (the “Enterprise License”)
Copyright (c) 2023 DanswerAI, Inc.
Copyright (c) 2023-present DanswerAI, Inc.
With regard to the Danswer Software:

View File

@@ -1,11 +1,11 @@
import { EE_ENABLED } from "@/lib/constants";
import { SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED } from "@/lib/constants";
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
if (!EE_ENABLED) {
if (!SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED) {
return (
<div className="flex h-screen">
<div className="mx-auto my-auto text-lg font-bold text-red-500">

View File

@@ -19,7 +19,7 @@ import {
getAuthTypeMetadataSS,
getCurrentUserSS,
} from "@/lib/userSS";
import { EE_ENABLED } from "@/lib/constants";
import { SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED } from "@/lib/constants";
import { redirect } from "next/navigation";
import {
FiActivity,
@@ -194,7 +194,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
),
link: "/admin/users",
},
...(EE_ENABLED
...(SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED
? [
{
name: (
@@ -227,7 +227,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
},
],
},
...(EE_ENABLED
...(SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED
? [
{
name: "Performance",
@@ -275,7 +275,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
),
link: "/admin/settings",
},
...(EE_ENABLED
...(SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED
? [
{
name: (

View File

@@ -1,3 +1,5 @@
"use client";
import React, { useState } from "react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
@@ -14,8 +16,8 @@ import { BooleanFormField, TextFormField } from "./Field";
import { createCredential, linkCredential } from "@/lib/credential";
import { useSWRConfig } from "swr";
import { Button, Divider } from "@tremor/react";
import { EE_ENABLED } from "@/lib/constants";
import IsPublicField from "./IsPublicField";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
const BASE_CONNECTOR_URL = "/api/manage/admin/connector";
@@ -76,7 +78,6 @@ interface BaseProps<T extends Yup.AnyObject> {
// If specified, then we will create an empty credential and associate
// the connector with it. If credentialId is specified, then this will be ignored
shouldCreateEmptyCredentialForConnector?: boolean;
showNonPublicOption?: boolean;
}
type ConnectorFormProps<T extends Yup.AnyObject> = RequireAtLeastOne<
@@ -98,12 +99,13 @@ export function ConnectorForm<T extends Yup.AnyObject>({
pruneFreq,
onSubmit,
shouldCreateEmptyCredentialForConnector,
// only show this option for EE, since groups are not supported in CE
showNonPublicOption = EE_ENABLED,
}: ConnectorFormProps<T>): JSX.Element {
const { mutate } = useSWRConfig();
const { popup, setPopup } = usePopup();
// only show this option for EE, since groups are not supported in CE
const showNonPublicOption = usePaidEnterpriseFeaturesEnabled();
const shouldHaveNameInput = credentialId !== undefined && !ccPairNameBuilder;
const ccPairNameInitialValue = shouldHaveNameInput

View File

@@ -1,10 +1,13 @@
import { EnterpriseSettings, Settings } from "@/app/admin/settings/interfaces";
import { CUSTOM_ANALYTICS_ENABLED, EE_ENABLED } from "@/lib/constants";
import {
CUSTOM_ANALYTICS_ENABLED,
SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED,
} from "@/lib/constants";
import { fetchSS } from "@/lib/utilsSS";
export async function fetchSettingsSS() {
const tasks = [fetchSS("/settings")];
if (EE_ENABLED) {
if (SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED) {
tasks.push(fetchSS("/enterprise-settings"));
if (CUSTOM_ANALYTICS_ENABLED) {
tasks.push(fetchSS("/enterprise-settings/custom-analytics-script"));

View File

@@ -0,0 +1,12 @@
"use client";
import { useContext } from "react";
import { SettingsContext } from "./SettingsProvider";
export function usePaidEnterpriseFeaturesEnabled() {
const combinedSettings = useContext(SettingsContext);
if (!combinedSettings) {
return null;
}
return combinedSettings.enterpriseSettings !== null;
}

View File

@@ -25,14 +25,13 @@ export const HEADER_PADDING = `pt-[64px]`;
export const LOGOUT_DISABLED =
process.env.NEXT_PUBLIC_DISABLE_LOGOUT?.toLowerCase() === "true";
// NOTE: since this is a `NEXT_PUBLIC_` variable, it will be set at
// build-time
// TODO: consider moving this to an API call so that the api_server
// can be the single source of truth
export const EE_ENABLED =
process.env.NEXT_PUBLIC_ENABLE_PAID_EE_FEATURES?.toLowerCase() === "true";
/* Enterprise-only settings */
// NOTE: this should ONLY be used on the server-side. If used client side,
// it will not be accurate (will always be false).
export const SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED =
process.env.ENABLE_PAID_EE_FEATURES?.toLowerCase() === "true";
// Enterprise-only settings
export const CUSTOM_ANALYTICS_ENABLED = process.env.CUSTOM_ANALYTICS_SECRET_KEY
? true
: false;

View File

@@ -11,10 +11,10 @@ import { errorHandlingFetcher } from "./fetcher";
import { useState } from "react";
import { DateRangePickerValue } from "@tremor/react";
import { SourceMetadata } from "./search/interfaces";
import { EE_ENABLED } from "./constants";
import { destructureValue } from "./llm/utils";
import { ChatSession } from "@/app/chat/interfaces";
import { UsersResponse } from "./users/interfaces";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
const CREDENTIAL_URL = "/api/manage/admin/credential";
@@ -192,8 +192,9 @@ export const useUserGroups = (): {
refreshUserGroups: () => void;
} => {
const swrResponse = useSWR<UserGroup[]>(USER_GROUP_URL, errorHandlingFetcher);
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
if (!EE_ENABLED) {
if (!isPaidEnterpriseFeaturesEnabled) {
return {
...{
data: [],

39
web/src/middleware.ts Normal file
View File

@@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED } from "./lib/constants";
const eePaths = [
"/admin/groups",
"/admin/api-key",
"/admin/performance/usage",
"/admin/performance/query-history",
"/admin/whitelabeling",
"/admin/performance/custom-analytics",
];
const eePathsForMatcher = eePaths.map((path) => `${path}/:path*`);
export async function middleware(request: NextRequest) {
if (SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED) {
const pathname = request.nextUrl.pathname;
// Check if the current path is in the eePaths list
if (eePaths.some((path) => pathname.startsWith(path))) {
// Add '/ee' to the beginning of the pathname
const newPathname = `/ee${pathname}`;
// Create a new URL with the modified pathname
const newUrl = new URL(newPathname, request.url);
// Rewrite to the new URL
return NextResponse.rewrite(newUrl);
}
}
// Continue with the response if no rewrite is needed
return NextResponse.next();
}
// Specify the paths that the middleware should run for
export const config = {
matcher: eePathsForMatcher,
};

View File

@@ -1,13 +1,11 @@
var merge = require("lodash/merge");
const baseThemes = require("./tailwind-themes/tailwind.config.js");
const customThemes =
process.env.NEXT_PUBLIC_ENABLE_PAID_EE_FEATURES &&
process.env.NEXT_PUBLIC_THEME
? require(
`./tailwind-themes/custom/${process.env.NEXT_PUBLIC_THEME}/tailwind.config.js`
)
: null;
const customThemes = process.env.NEXT_PUBLIC_THEME
? require(
`./tailwind-themes/custom/${process.env.NEXT_PUBLIC_THEME}/tailwind.config.js`
)
: null;
/** @type {import('tailwindcss').Config} */
module.exports = customThemes ? merge(baseThemes, customThemes) : baseThemes;