Compare commits

...

5 Commits

Author SHA1 Message Date
pablonyx
02cf02b5dd k 2025-03-04 17:16:50 -08:00
pablonyx
3e2a08a42a k 2025-03-04 17:13:49 -08:00
pablonyx
da17b4aa36 k 2025-03-04 17:10:25 -08:00
pablonyx
4142ae0afd k 2025-03-04 14:11:32 -08:00
pablonyx
5d857a5112 k 2025-03-03 22:17:28 -08:00
13 changed files with 1802 additions and 547 deletions

433
web/package-lock.json generated
View File

@@ -44,6 +44,7 @@
"autoprefixer": "^10.4.14",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"favicon-fetch": "^1.0.0",
"formik": "^2.2.9",
@@ -9313,6 +9314,438 @@
"node": ">=6"
}
},
"node_modules/cmdk": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz",
"integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dialog": "1.0.5",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/cmdk/node_modules/@radix-ui/primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
"integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-context": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-dialog": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz",
"integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-controllable-state": "1.0.1",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
"integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-escape-keydown": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz",
"integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
"integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
"integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
"integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-id/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-portal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
"integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-presence": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
"integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-presence/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-primitive": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
"integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/react-remove-scroll": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
"integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.3",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",

View File

@@ -47,6 +47,7 @@
"autoprefixer": "^10.4.14",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"favicon-fetch": "^1.0.0",
"formik": "^2.2.9",

View File

@@ -1,6 +1,6 @@
"use client";
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
import { Form, Formik } from "formik";
import * as Yup from "yup";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import {
@@ -10,13 +10,14 @@ import {
} from "./lib";
import { ConnectorStatus, DocumentSet, UserGroup, UserRole } from "@/lib/types";
import { TextFormField } from "@/components/admin/connectors/Field";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { IsPublicGroupSelector } from "@/components/IsPublicGroupSelector";
import React, { useEffect, useState } from "react";
import { useUser } from "@/components/user/UserProvider";
import { ConnectorMultiSelect } from "@/components/ConnectorMultiSelect";
import { NonSelectableConnectors } from "@/components/NonSelectableConnectors";
interface SetCreationPopupProps {
ccPairs: ConnectorStatus<any, any>[];
@@ -45,7 +46,7 @@ export const DocumentSetCreationForm = ({
}, [existingDocumentSet?.is_public]);
return (
<div>
<div className="max-w-full mx-auto">
<Formik<DocumentSetCreationRequest>
initialValues={{
name: existingDocumentSet?.name ?? "",
@@ -104,243 +105,122 @@ export const DocumentSetCreationForm = ({
}}
>
{(props) => {
// Filter visible cc pairs for curator role
const visibleCcPairs =
user?.role === UserRole.CURATOR
? localCcPairs.filter(
(ccPair) =>
ccPair.access_type === "public" ||
(ccPair.groups.length > 0 &&
props.values.groups.every((group) =>
ccPair.groups.includes(group)
))
)
: localCcPairs;
// Filter non-visible cc pairs for curator role
const nonVisibleCcPairs =
user?.role === UserRole.CURATOR
? localCcPairs.filter(
(ccPair) =>
!(ccPair.access_type === "public") &&
(ccPair.groups.length === 0 ||
!props.values.groups.every((group) =>
ccPair.groups.includes(group)
))
)
: [];
// Deselect filtered out cc pairs
if (user?.role === UserRole.CURATOR) {
const visibleCcPairIds = visibleCcPairs.map(
(ccPair) => ccPair.cc_pair_id
);
props.values.cc_pair_ids = props.values.cc_pair_ids.filter((id) =>
visibleCcPairIds.includes(id)
);
}
return (
<Form>
<TextFormField
name="name"
label="Name:"
placeholder="A name for the document set"
disabled={isUpdate}
autoCompleteDisabled={true}
/>
<TextFormField
name="description"
label="Description:"
placeholder="Describe what the document set represents"
autoCompleteDisabled={true}
optional={true}
/>
{isPaidEnterpriseFeaturesEnabled && (
<IsPublicGroupSelector
formikProps={props}
objectName="document set"
<Form className="space-y-6 w-full ">
<div className="space-y-4 w-full">
<TextFormField
name="name"
label="Name:"
placeholder="A name for the document set"
disabled={isUpdate}
autoCompleteDisabled={true}
/>
<TextFormField
name="description"
label="Description:"
placeholder="Describe what the document set represents"
autoCompleteDisabled={true}
optional={true}
/>
)}
<Separator />
{user?.role === UserRole.CURATOR ? (
<>
<div className="flex flex-col gap-y-1">
<h2 className="mb-1 font-medium text-base">
These are the connectors available to{" "}
{userGroups && userGroups.length > 1
? "the selected group"
: "the group you curate"}
:
</h2>
<p className="mb-text-sm">
All documents indexed by these selected connectors will be
a part of this document set.
</p>
<FieldArray
name="cc_pair_ids"
render={(arrayHelpers: ArrayHelpers) => {
// Filter visible cc pairs
const visibleCcPairs = localCcPairs.filter(
(ccPair) =>
ccPair.access_type === "public" ||
(ccPair.groups.length > 0 &&
props.values.groups.every((group) =>
ccPair.groups.includes(group)
))
);
// Deselect filtered out cc pairs
const visibleCcPairIds = visibleCcPairs.map(
(ccPair) => ccPair.cc_pair_id
);
props.values.cc_pair_ids =
props.values.cc_pair_ids.filter((id) =>
visibleCcPairIds.includes(id)
);
return (
<div className="mb-3 flex gap-2 flex-wrap">
{visibleCcPairs.map((ccPair) => {
const ind = props.values.cc_pair_ids.indexOf(
ccPair.cc_pair_id
);
const isSelected = ind !== -1;
return (
<div
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
className={
`
px-3
py-1
rounded-lg
border
border-border
w-fit
flex
cursor-pointer ` +
(isSelected
? " bg-background-200"
: " hover:bg-accent-background-hovered")
}
onClick={() => {
if (isSelected) {
arrayHelpers.remove(ind);
} else {
arrayHelpers.push(ccPair.cc_pair_id);
}
}}
>
<div className="my-auto">
<ConnectorTitle
connector={ccPair.connector}
ccPairId={ccPair.cc_pair_id}
ccPairName={ccPair.name}
isLink={false}
showMetadata={false}
/>
</div>
</div>
);
})}
</div>
);
}}
/>
</div>
<div>
<FieldArray
name="cc_pair_ids"
render={() => {
// Filter non-visible cc pairs
const nonVisibleCcPairs = localCcPairs.filter(
(ccPair) =>
!(ccPair.access_type === "public") &&
(ccPair.groups.length === 0 ||
!props.values.groups.every((group) =>
ccPair.groups.includes(group)
))
);
return nonVisibleCcPairs.length > 0 ? (
<>
<Separator />
<h2 className="mb-1 font-medium text-base">
These connectors are not available to the{" "}
{userGroups && userGroups.length > 1
? `group${
props.values.groups.length > 1 ? "s" : ""
} you have selected`
: "group you curate"}
:
</h2>
<p className="mb-3 text-sm">
Only connectors that are directly assigned to the
group you are trying to add the document set to
will be available.
</p>
<div className="mb-3 flex gap-2 flex-wrap">
{nonVisibleCcPairs.map((ccPair) => (
<div
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
className="px-3 py-1 rounded-lg border border-non-selectable-border w-fit flex cursor-not-allowed"
>
<div className="my-auto">
<ConnectorTitle
connector={ccPair.connector}
ccPairId={ccPair.cc_pair_id}
ccPairName={ccPair.name}
isLink={false}
showMetadata={false}
/>
</div>
</div>
))}
</div>
</>
) : null;
}}
/>
</div>
</>
) : (
<div>
<h2 className="mb-1 font-medium text-base">
Pick your connectors:
</h2>
<p className="mb-3 text-xs">
All documents indexed by the selected connectors will be a
part of this document set.
</p>
<FieldArray
name="cc_pair_ids"
render={(arrayHelpers: ArrayHelpers) => (
<div className="mb-3 flex gap-2 flex-wrap">
{ccPairs.map((ccPair) => {
const ind = props.values.cc_pair_ids.indexOf(
ccPair.cc_pair_id
);
const isSelected = ind !== -1;
return (
<div
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
className={
`
px-3
py-1
rounded-lg
border
border-border
w-fit
flex
cursor-pointer ` +
(isSelected
? " bg-background-200"
: " hover:bg-accent-background-hovered")
}
onClick={() => {
if (isSelected) {
arrayHelpers.remove(ind);
} else {
arrayHelpers.push(ccPair.cc_pair_id);
}
}}
>
<div className="my-auto">
<ConnectorTitle
connector={ccPair.connector}
ccPairId={ccPair.cc_pair_id}
ccPairName={ccPair.name}
isLink={false}
showMetadata={false}
/>
</div>
</div>
);
})}
</div>
)}
{isPaidEnterpriseFeaturesEnabled && (
<IsPublicGroupSelector
formikProps={props}
objectName="document set"
/>
</div>
)}
)}
</div>
<div className="flex mt-6">
<Separator className="my-6" />
<div className="space-y-6">
{user?.role === UserRole.CURATOR ? (
<>
<ConnectorMultiSelect
name="cc_pair_ids"
label={`Connectors available to ${
userGroups && userGroups.length > 1
? "the selected group"
: "the group you curate"
}`}
connectors={visibleCcPairs}
selectedIds={props.values.cc_pair_ids}
onChange={(selectedIds) => {
props.setFieldValue("cc_pair_ids", selectedIds);
}}
placeholder="Search for connectors..."
/>
<NonSelectableConnectors
connectors={nonVisibleCcPairs}
title={`Connectors not available to the ${
userGroups && userGroups.length > 1
? `group${
props.values.groups.length > 1 ? "s" : ""
} you have selected`
: "group you curate"
}`}
description="Only connectors that are directly assigned to the group you are trying to add the document set to will be available."
/>
</>
) : (
<ConnectorMultiSelect
name="cc_pair_ids"
label="Pick your connectors"
connectors={visibleCcPairs}
selectedIds={props.values.cc_pair_ids}
onChange={(selectedIds) => {
props.setFieldValue("cc_pair_ids", selectedIds);
}}
placeholder="Search for connectors..."
/>
)}
</div>
<div className="flex mt-6 pt-4 border-t border-neutral-200">
<Button
type="submit"
variant="submit"
disabled={props.isSubmitting}
className="w-64 mx-auto"
className="w-56 mx-auto py-1.5 h-auto text-sm"
>
{isUpdate ? "Update!" : "Create!"}
{isUpdate ? "Update Document Set" : "Create Document Set"}
</Button>
</div>
</Form>

View File

@@ -26,6 +26,8 @@ import {
FiUnlock,
FiRefreshCw,
FiPauseCircle,
FiFilter,
FiX,
} from "react-icons/fi";
import {
Tooltip,
@@ -41,6 +43,7 @@ import Cookies from "js-cookie";
import { TOGGLED_CONNECTORS_COOKIE_NAME } from "@/lib/constants";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { ConnectorCredentialPairStatus } from "../../connector/[ccPairId]/types";
import { FilterComponent, FilterOptions } from "./FilterComponent";
function SummaryRow({
source,
@@ -285,7 +288,26 @@ export function CCPairIndexingStatusTable({
return savedState ? JSON.parse(savedState) : {};
});
const { groupedStatuses, sortedSources, groupSummaries } = useMemo(() => {
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
accessType: null,
docsCountFilter: {
operator: null,
value: null,
},
lastStatus: null,
});
// Reference to the FilterComponent for resetting its state
const filterComponentRef = useRef<{
resetFilters: () => void;
} | null>(null);
const {
groupedStatuses,
sortedSources,
groupSummaries,
filteredGroupedStatuses,
} = useMemo(() => {
const grouped: Record<ValidSources, ConnectorIndexingStatus<any, any>[]> =
{} as Record<ValidSources, ConnectorIndexingStatus<any, any>[]>;
@@ -337,12 +359,139 @@ export function CCPairIndexingStatusTable({
};
});
// Apply filters to create filtered grouped statuses
const filteredGrouped: Record<
ValidSources,
ConnectorIndexingStatus<any, any>[]
> = {} as Record<ValidSources, ConnectorIndexingStatus<any, any>[]>;
sorted.forEach((source) => {
const statuses = grouped[source];
// Apply filters
const filteredStatuses = statuses.filter((status) => {
// Filter by access type
if (filterOptions.accessType && filterOptions.accessType.length > 0) {
if (!filterOptions.accessType.includes(status.access_type)) {
return false;
}
}
// Filter by last status
if (filterOptions.lastStatus && filterOptions.lastStatus.length > 0) {
if (
!filterOptions.lastStatus.includes(
status.last_finished_status as any
)
) {
return false;
}
}
// Filter by docs count
if (filterOptions.docsCountFilter.operator) {
const { operator, value } = filterOptions.docsCountFilter;
// If only operator is selected (no value), show all
if (value === null) {
return true;
}
if (operator === ">" && !(status.docs_indexed > value)) {
return false;
} else if (operator === "<" && !(status.docs_indexed < value)) {
return false;
} else if (operator === "=" && status.docs_indexed !== value) {
return false;
}
}
return true;
});
if (filteredStatuses.length > 0) {
filteredGrouped[source] = filteredStatuses;
}
});
return {
groupedStatuses: grouped,
sortedSources: sorted,
groupSummaries: summaries,
filteredGroupedStatuses: filteredGrouped,
};
}, [ccPairsIndexingStatuses, editableCcPairsIndexingStatuses]);
}, [ccPairsIndexingStatuses, editableCcPairsIndexingStatuses, filterOptions]);
// Determine which sources to display based on filters and search
const displaySources = useMemo(() => {
const hasActiveFilters =
(filterOptions.accessType && filterOptions.accessType.length > 0) ||
(filterOptions.lastStatus && filterOptions.lastStatus.length > 0) ||
filterOptions.docsCountFilter.operator !== null;
if (hasActiveFilters) {
return Object.keys(filteredGroupedStatuses) as ValidSources[];
}
return sortedSources;
}, [sortedSources, filteredGroupedStatuses, filterOptions]);
const handleFilterChange = (newFilters: FilterOptions) => {
setFilterOptions(newFilters);
// Auto-expand sources when filters are applied
if (
(newFilters.accessType && newFilters.accessType.length > 0) ||
(newFilters.lastStatus && newFilters.lastStatus.length > 0) ||
newFilters.docsCountFilter.operator !== null
) {
// We need to wait for the filteredGroupedStatuses to be updated
// before we can expand the sources
setTimeout(() => {
const sourcesToExpand = Object.keys(
filteredGroupedStatuses
) as ValidSources[];
const newConnectorsToggled = { ...connectorsToggled };
sourcesToExpand.forEach((source) => {
newConnectorsToggled[source] = true;
});
setConnectorsToggled(newConnectorsToggled);
Cookies.set(
TOGGLED_CONNECTORS_COOKIE_NAME,
JSON.stringify(newConnectorsToggled)
);
}, 0);
}
};
const clearAllFilters = () => {
const emptyFilters: FilterOptions = {
accessType: null,
docsCountFilter: {
operator: null,
value: null,
},
lastStatus: null,
};
setFilterOptions(emptyFilters);
// Reset the FilterComponent's internal state
if (filterComponentRef.current) {
filterComponentRef.current.resetFilters();
}
};
// Check if filters are active
const hasActiveFilters = useMemo(() => {
return (
(filterOptions.accessType && filterOptions.accessType.length > 0) ||
(filterOptions.lastStatus && filterOptions.lastStatus.length > 0) ||
filterOptions.docsCountFilter.operator !== null
);
}, [filterOptions]);
const toggleSource = (
source: ValidSources,
@@ -376,127 +525,194 @@ export function CCPairIndexingStatusTable({
sortedSources.length;
return (
<Table>
<TableHeader>
<ConnectorRow
invisible
ccPairsIndexingStatus={{
cc_pair_id: 1,
name: "Sample File Connector",
cc_pair_status: ConnectorCredentialPairStatus.ACTIVE,
last_status: "success",
connector: {
<>
<Table>
<TableHeader>
<ConnectorRow
invisible
ccPairsIndexingStatus={{
cc_pair_id: 1,
name: "Sample File Connector",
source: ValidSources.File,
input_type: "poll",
connector_specific_config: {
file_locations: ["/path/to/sample/file.txt"],
cc_pair_status: ConnectorCredentialPairStatus.ACTIVE,
last_status: "success",
connector: {
name: "Sample File Connector",
source: ValidSources.File,
input_type: "poll",
connector_specific_config: {
file_locations: ["/path/to/sample/file.txt"],
},
refresh_freq: 86400,
prune_freq: null,
indexing_start: new Date("2023-07-01T12:00:00Z"),
id: 1,
credential_ids: [],
access_type: "public",
time_created: "2023-07-01T12:00:00Z",
time_updated: "2023-07-01T12:00:00Z",
},
credential: {
id: 1,
name: "Sample Credential",
source: ValidSources.File,
user_id: "1",
time_created: "2023-07-01T12:00:00Z",
time_updated: "2023-07-01T12:00:00Z",
credential_json: {},
admin_public: false,
},
refresh_freq: 86400,
prune_freq: null,
indexing_start: new Date("2023-07-01T12:00:00Z"),
id: 1,
credential_ids: [],
access_type: "public",
time_created: "2023-07-01T12:00:00Z",
time_updated: "2023-07-01T12:00:00Z",
},
credential: {
id: 1,
name: "Sample Credential",
source: ValidSources.File,
user_id: "1",
time_created: "2023-07-01T12:00:00Z",
time_updated: "2023-07-01T12:00:00Z",
credential_json: {},
admin_public: false,
},
access_type: "public",
docs_indexed: 1000,
last_success: "2023-07-01T12:00:00Z",
last_finished_status: "success",
latest_index_attempt: null,
groups: [], // Add this line
}}
isEditable={false}
/>
</TableHeader>
<div className="flex -mt-12 items-center w-0 m4 gap-x-2">
<input
type="text"
ref={searchInputRef}
placeholder="Search connectors..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="ml-1 w-96 h-9 border border-border flex-none rounded-md bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
docs_indexed: 1000,
last_success: "2023-07-01T12:00:00Z",
last_finished_status: "success",
latest_index_attempt: null,
groups: [], // Add this line
}}
isEditable={false}
/>
</TableHeader>
<div className="flex -mt-12 items-center w-0 m4 gap-x-2">
<input
type="text"
ref={searchInputRef}
placeholder="Search connectors..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="ml-1 w-96 h-9 border border-border flex-none rounded-md bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
<Button className="h-9" onClick={() => toggleSources()}>
{!shouldExpand ? "Collapse All" : "Expand All"}
</Button>
</div>
<TableBody>
{sortedSources
.filter(
(source) => source != "not_applicable" && source != "ingestion_api"
)
.map((source, ind) => {
const sourceMatches = source
.toLowerCase()
.includes(searchTerm.toLowerCase());
const matchingConnectors = groupedStatuses[source].filter(
(status) =>
<Button className="h-9" onClick={() => toggleSources()}>
{!shouldExpand ? "Collapse All" : "Expand All"}
</Button>
<div className="flex items-center gap-2">
<FilterComponent
onFilterChange={handleFilterChange}
ref={filterComponentRef}
/>
{hasActiveFilters && (
<div className="flex flex-none items-center gap-1 ml-2 max-w-[500px]">
{filterOptions.accessType &&
filterOptions.accessType.length > 0 && (
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
Access: {filterOptions.accessType.join(", ")}
</Badge>
)}
{filterOptions.lastStatus &&
filterOptions.lastStatus.length > 0 && (
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
Status:{" "}
{filterOptions.lastStatus
.map((s) => s.replace(/_/g, " "))
.join(", ")}
</Badge>
)}
{filterOptions.docsCountFilter.operator &&
filterOptions.docsCountFilter.value !== null && (
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
Docs {filterOptions.docsCountFilter.operator}{" "}
{filterOptions.docsCountFilter.value}
</Badge>
)}
{filterOptions.docsCountFilter.operator &&
filterOptions.docsCountFilter.value === null && (
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
Docs {filterOptions.docsCountFilter.operator} any
</Badge>
)}
<Badge
variant="outline"
className="px-2 py-0.5 text-xs border-red-400 bg-red-100 hover:border-red-600 cursor-pointer hover:bg-red-100 dark:hover:bg-red-900"
onClick={() => {
if (filterComponentRef.current) {
filterComponentRef.current.resetFilters();
setFilterOptions({
accessType: null,
docsCountFilter: {
operator: null,
value: null,
},
lastStatus: null,
});
}
}}
>
<span className="text-red-500 dark:text-red-400">Clear</span>
</Badge>
</div>
)}
</div>
</div>
<TableBody>
{displaySources
.filter(
(source) =>
source != "not_applicable" && source != "ingestion_api"
)
.map((source, ind) => {
const sourceMatches = source
.toLowerCase()
.includes(searchTerm.toLowerCase());
const statuses =
filteredGroupedStatuses[source] || groupedStatuses[source];
const matchingConnectors = statuses.filter((status) =>
(status.name || "")
.toLowerCase()
.includes(searchTerm.toLowerCase())
);
if (sourceMatches || matchingConnectors.length > 0) {
return (
<React.Fragment key={ind}>
<br className="mt-4" />
<SummaryRow
source={source}
summary={groupSummaries[source]}
isOpen={connectorsToggled[source] || false}
onToggle={() => toggleSource(source)}
/>
{connectorsToggled[source] && (
<>
<TableRow
noHover
className="border ! border-border dark:border-neutral-700"
>
<TableHead>Name</TableHead>
<TableHead>Last Indexed</TableHead>
<TableHead>Activity</TableHead>
{isPaidEnterpriseFeaturesEnabled && (
<TableHead>Permissions</TableHead>
)}
<TableHead>Total Docs</TableHead>
<TableHead>Last Status</TableHead>
<TableHead></TableHead>
</TableRow>
{(sourceMatches
? groupedStatuses[source]
: matchingConnectors
).map((ccPairsIndexingStatus) => (
<ConnectorRow
key={ccPairsIndexingStatus.cc_pair_id}
ccPairsIndexingStatus={ccPairsIndexingStatus}
isEditable={editableCcPairsIndexingStatuses.some(
(e) =>
e.cc_pair_id === ccPairsIndexingStatus.cc_pair_id
)}
/>
))}
</>
)}
</React.Fragment>
);
}
return null;
})}
</TableBody>
</Table>
if (sourceMatches || matchingConnectors.length > 0) {
return (
<React.Fragment key={ind}>
<br className="mt-4" />
<SummaryRow
source={source}
summary={groupSummaries[source]}
isOpen={connectorsToggled[source] || false}
onToggle={() => toggleSource(source)}
/>
{connectorsToggled[source] && (
<>
<TableRow className="border border-border dark:border-neutral-700">
<TableHead>Name</TableHead>
<TableHead>Last Indexed</TableHead>
<TableHead>Activity</TableHead>
{isPaidEnterpriseFeaturesEnabled && (
<TableHead>Permissions</TableHead>
)}
<TableHead>Total Docs</TableHead>
<TableHead>Last Status</TableHead>
<TableHead></TableHead>
</TableRow>
{(sourceMatches ? statuses : matchingConnectors).map(
(ccPairsIndexingStatus) => (
<ConnectorRow
key={ccPairsIndexingStatus.cc_pair_id}
ccPairsIndexingStatus={ccPairsIndexingStatus}
isEditable={editableCcPairsIndexingStatuses.some(
(e) =>
e.cc_pair_id ===
ccPairsIndexingStatus.cc_pair_id
)}
/>
)
)}
</>
)}
</React.Fragment>
);
}
return null;
})}
</TableBody>
</Table>
</>
);
}

View File

@@ -0,0 +1,375 @@
"use client";
import React, { useState, useImperativeHandle, forwardRef } from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
} from "@/components/ui/dropdown-menu";
import { SortIcon } from "@/components/icons/icons";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { AccessType, ValidStatuses } from "@/lib/types";
import { FiFilter, FiX, FiCheck } from "react-icons/fi";
export interface FilterOptions {
accessType: AccessType[] | null;
docsCountFilter: {
operator: ">" | "<" | "=" | null;
value: number | null;
};
lastStatus: ValidStatuses[] | null;
}
interface FilterComponentProps {
onFilterChange: (filters: FilterOptions) => void;
}
export const FilterComponent = forwardRef<
{ resetFilters: () => void },
FilterComponentProps
>(({ onFilterChange }, ref) => {
const [isOpen, setIsOpen] = useState(false);
const [filters, setFilters] = useState<FilterOptions>({
accessType: null,
docsCountFilter: {
operator: null,
value: null,
},
lastStatus: null,
});
// Local state for tracking selected filters before applying
const [docsOperator, setDocsOperator] = useState<">" | "<" | "=" | null>(
null
);
const [docsValue, setDocsValue] = useState<string>("");
const [selectedAccessTypes, setSelectedAccessTypes] = useState<AccessType[]>(
[]
);
const [selectedStatuses, setSelectedStatuses] = useState<ValidStatuses[]>([]);
// Expose resetFilters method via ref
useImperativeHandle(ref, () => ({
resetFilters: () => {
setDocsOperator(null);
setDocsValue("");
setSelectedAccessTypes([]);
setSelectedStatuses([]);
setFilters({
accessType: null,
docsCountFilter: {
operator: null,
value: null,
},
lastStatus: null,
});
},
}));
const handleAccessTypeChange = (accessType: AccessType) => {
const newAccessTypes = selectedAccessTypes.includes(accessType)
? selectedAccessTypes.filter((type) => type !== accessType)
: [...selectedAccessTypes, accessType];
setSelectedAccessTypes(newAccessTypes);
};
const handleStatusChange = (status: ValidStatuses) => {
const newStatuses = selectedStatuses.includes(status)
? selectedStatuses.filter((s) => s !== status)
: [...selectedStatuses, status];
setSelectedStatuses(newStatuses);
};
const handleDocsFilterChange = () => {
if (docsOperator && docsValue) {
const newFilters = {
...filters,
accessType: selectedAccessTypes.length > 0 ? selectedAccessTypes : null,
lastStatus: selectedStatuses.length > 0 ? selectedStatuses : null,
docsCountFilter: {
operator: docsOperator,
value: parseInt(docsValue),
},
};
setFilters(newFilters);
onFilterChange(newFilters);
setIsOpen(false);
}
};
const applyFilters = () => {
const newFilters = {
...filters,
accessType: selectedAccessTypes.length > 0 ? selectedAccessTypes : null,
lastStatus: selectedStatuses.length > 0 ? selectedStatuses : null,
docsCountFilter: {
operator: docsOperator,
value: docsValue ? parseInt(docsValue) : null,
},
};
setFilters(newFilters);
onFilterChange(newFilters);
setIsOpen(false);
};
const clearFilters = () => {
setSelectedAccessTypes([]);
setSelectedStatuses([]);
setDocsOperator(null);
setDocsValue("");
const newFilters = {
accessType: null,
docsCountFilter: {
operator: null,
value: null,
},
lastStatus: null,
};
setFilters(newFilters);
onFilterChange(newFilters);
};
// Sync local state with filters when dropdown opens
const handleOpenChange = (open: boolean) => {
if (open) {
// When opening, initialize local state from current filters
setSelectedAccessTypes(filters.accessType || []);
setSelectedStatuses(filters.lastStatus || []);
setDocsOperator(filters.docsCountFilter.operator);
setDocsValue(
filters.docsCountFilter.value !== null
? filters.docsCountFilter.value.toString()
: ""
);
}
setIsOpen(open);
};
const hasActiveFilters =
(filters.accessType && filters.accessType.length > 0) ||
(filters.lastStatus && filters.lastStatus.length > 0) ||
filters.docsCountFilter.operator !== null;
// Get active filter count for badge
const getActiveFilterCount = () => {
let count = 0;
if (filters.accessType && filters.accessType.length > 0) count++;
if (filters.lastStatus && filters.lastStatus.length > 0) count++;
if (filters.docsCountFilter.operator !== null) count++;
return count;
};
return (
<div className="relative">
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className={`p-2 h-9 ${
hasActiveFilters ? "border-primary bg-primary/5" : ""
}`}
>
<SortIcon size={20} className="text-neutral-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-72"
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div className="flex items-center justify-between px-2 py-1.5">
<DropdownMenuLabel className="text-base font-medium">
Filter Connectors
</DropdownMenuLabel>
</div>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="px-2 py-1.5 text-xs text-muted-foreground">
Access Type
</DropdownMenuLabel>
<div onClick={(e) => e.stopPropagation()}>
<DropdownMenuCheckboxItem
checked={selectedAccessTypes.includes("public")}
onCheckedChange={() => handleAccessTypeChange("public")}
className="flex items-center justify-between"
onSelect={(e) => e.preventDefault()}
>
Public
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={selectedAccessTypes.includes("private")}
onCheckedChange={() => handleAccessTypeChange("private")}
className="flex items-center justify-between"
onSelect={(e) => e.preventDefault()}
>
Private
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={selectedAccessTypes.includes("sync")}
onCheckedChange={() => handleAccessTypeChange("sync")}
className="flex items-center justify-between"
onSelect={(e) => e.preventDefault()}
>
Auto-Sync
</DropdownMenuCheckboxItem>
</div>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="px-2 py-1.5 text-xs text-muted-foreground">
Last Status
</DropdownMenuLabel>
<div onClick={(e) => e.stopPropagation()}>
<DropdownMenuCheckboxItem
checked={selectedStatuses.includes("success")}
onCheckedChange={() => handleStatusChange("success")}
className="flex items-center justify-between"
onSelect={(e) => e.preventDefault()}
>
Success
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={selectedStatuses.includes("failed")}
onCheckedChange={() => handleStatusChange("failed")}
className="flex items-center justify-between"
onSelect={(e) => e.preventDefault()}
>
Failed
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={selectedStatuses.includes("in_progress")}
onCheckedChange={() => handleStatusChange("in_progress")}
className="flex items-center justify-between"
onSelect={(e) => e.preventDefault()}
>
In Progress
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={selectedStatuses.includes("not_started")}
onCheckedChange={() => handleStatusChange("not_started")}
className="flex items-center justify-between"
onSelect={(e) => e.preventDefault()}
>
Not Started
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={selectedStatuses.includes("completed_with_errors")}
onCheckedChange={() =>
handleStatusChange("completed_with_errors")
}
className="flex items-center justify-between"
onSelect={(e) => e.preventDefault()}
>
Completed with Errors
</DropdownMenuCheckboxItem>
</div>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="px-2 py-1.5 text-xs text-muted-foreground">
Document Count
</DropdownMenuLabel>
<div
className="flex items-center px-2 py-2 gap-2"
onClick={(e) => e.stopPropagation()}
>
<div className="flex gap-2">
<Button
variant={docsOperator === ">" ? "default" : "outline"}
size="sm"
className="h-8 px-2"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDocsOperator(docsOperator === ">" ? null : ">");
}}
type="button"
>
&gt;
</Button>
<Button
variant={docsOperator === "<" ? "default" : "outline"}
size="sm"
className="h-8 px-2"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDocsOperator(docsOperator === "<" ? null : "<");
}}
type="button"
>
&lt;
</Button>
<Button
variant={docsOperator === "=" ? "default" : "outline"}
size="sm"
className="h-8 px-2"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDocsOperator(docsOperator === "=" ? null : "=");
}}
type="button"
>
=
</Button>
</div>
<Input
type="number"
placeholder="Count"
value={docsValue}
onChange={(e) => setDocsValue(e.target.value)}
className="h-8 w-full"
onClick={(e) => e.stopPropagation()}
/>
</div>
<div className="px-2 py-1.5">
<Button
size="sm"
className="w-full h-8"
disabled={false}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
applyFilters();
}}
type="button"
>
Apply
</Button>
</div>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
{hasActiveFilters && (
<div className="absolute -top-1 -right-1">
<Badge className="h-2 bg-red-400 border-red-400 w-2 p-0 border-2 flex items-center justify-center" />
</div>
)}
</div>
);
});
FilterComponent.displayName = "FilterComponent";

View File

@@ -403,7 +403,8 @@ export function ChatInputBar({
setTabbingIconIndex((tabbingIconIndex) =>
Math.min(
tabbingIconIndex + 1,
showPrompts ? filteredPrompts.length : assistantTagOptions.length
// showPrompts ? filteredPrompts.length :
assistantTagOptions.length
)
);
} else if (e.key === "ArrowUp") {
@@ -436,8 +437,8 @@ export function ChatInputBar({
<button
key={index}
className={`px-2 ${
tabbingIconIndex == index && "bg-background-dark/75"
} rounded items-center rounded-lg content-start flex gap-x-1 py-2 w-full hover:bg-background-dark/90 cursor-pointer`}
tabbingIconIndex == index && "bg-neutral-200"
} rounded items-center rounded-lg content-start flex gap-x-1 py-2 w-full hover:bg-neutral-200/90 cursor-pointer`}
onClick={() => {
updatedTaggedAssistant(currentAssistant);
}}
@@ -459,8 +460,8 @@ export function ChatInputBar({
target="_self"
className={`${
tabbingIconIndex == assistantTagOptions.length &&
"bg-background-dark/75"
} rounded rounded-lg px-3 flex gap-x-1 py-2 w-full items-center hover:bg-background-dark/90 cursor-pointer`}
"bg-neutral-200"
} rounded rounded-lg px-3 flex gap-x-1 py-2 w-full items-center hover:bg-neutral-200/90 cursor-pointer`}
href="/assistants/new"
>
<FiPlus size={17} />

View File

@@ -1,5 +1,5 @@
import { ConnectorIndexingStatus, ConnectorStatus } from "@/lib/types";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { ConnectorStatus } from "@/lib/types";
import { ConnectorMultiSelect } from "@/components/ConnectorMultiSelect";
interface ConnectorEditorProps {
selectedCCPairIds: number[];
@@ -12,55 +12,20 @@ export const ConnectorEditor = ({
setSetCCPairIds,
allCCPairs,
}: ConnectorEditorProps) => {
// Filter out public docs, since they don't make sense as part of a group
const privateCCPairs = allCCPairs.filter(
(ccPair) => ccPair.access_type === "private"
);
return (
<div className="mb-3 flex gap-2 flex-wrap">
{allCCPairs
// remove public docs, since they don't make sense as part of a group
.filter((ccPair) => !(ccPair.access_type === "public"))
.map((ccPair) => {
const ind = selectedCCPairIds.indexOf(ccPair.cc_pair_id);
const isSelected = ind !== -1;
return (
<div
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
className={
`
px-3
py-1
rounded-lg
border
border-border
w-fit
flex
cursor-pointer ` +
(isSelected
? " bg-accent-background-hovered"
: " hover:bg-accent-background")
}
onClick={() => {
if (isSelected) {
setSetCCPairIds(
selectedCCPairIds.filter(
(ccPairId) => ccPairId !== ccPair.cc_pair_id
)
);
} else {
setSetCCPairIds([...selectedCCPairIds, ccPair.cc_pair_id]);
}
}}
>
<div className="my-auto">
<ConnectorTitle
connector={ccPair.connector}
ccPairId={ccPair.cc_pair_id}
ccPairName={ccPair.name}
isLink={false}
showMetadata={false}
/>
</div>
</div>
);
})}
</div>
<ConnectorMultiSelect
name="connectors"
label="Connectors"
connectors={privateCCPairs}
selectedIds={selectedCCPairIds}
onChange={setSetCCPairIds}
placeholder="Search for connectors..."
showError={true}
/>
);
};

View File

@@ -1,13 +1,15 @@
import { Button } from "@/components/Button";
import { SearchMultiSelectDropdown } from "@/components/Dropdown";
import { Modal } from "@/components/Modal";
import { useState } from "react";
import { FiPlus, FiX } from "react-icons/fi";
import { FiX } from "react-icons/fi";
import { updateUserGroup } from "./lib";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { ConnectorStatus, UserGroup } from "@/lib/types";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { Connector } from "@/lib/connectors/connectors";
import { ConnectorMultiSelect } from "@/components/ConnectorMultiSelect";
import { Form } from "formik";
interface AddConnectorFormProps {
ccPairs: ConnectorStatus<any, any>[];
userGroup: UserGroup;
@@ -23,132 +25,68 @@ export const AddConnectorForm: React.FC<AddConnectorFormProps> = ({
}) => {
const [selectedCCPairIds, setSelectedCCPairIds] = useState<number[]>([]);
const selectedCCPairs = ccPairs.filter((ccPair) =>
selectedCCPairIds.includes(ccPair.cc_pair_id)
);
return (
<Modal title="Add New Connector" onOutsideClick={() => onClose()}>
<div className="px-6 pt-4 pb-12">
<div className="mb-2 flex flex-wrap gap-x-2">
{selectedCCPairs.length > 0 &&
selectedCCPairs.map((ccPair) => (
<div
key={ccPair.cc_pair_id}
onClick={() => {
setSelectedCCPairIds(
selectedCCPairIds.filter(
(ccPairId) => ccPairId !== ccPair.cc_pair_id
)
);
}}
className={`
flex
rounded-lg
px-2
py-1
my-1
border
border-border
hover:bg-accent-background-hovered
cursor-pointer`}
>
<ConnectorTitle
ccPairId={ccPair.cc_pair_id}
ccPairName={ccPair.name}
connector={ccPair.connector}
isLink={false}
showMetadata={false}
/>
<FiX className="ml-1 my-auto" />
</div>
))}
</div>
// Filter out ccPairs that are already in the user group and are not private
const availableCCPairs = ccPairs
.filter(
(ccPair) =>
!userGroup.cc_pairs
.map((userGroupCCPair) => userGroupCCPair.id)
.includes(ccPair.cc_pair_id)
)
.filter((ccPair) => ccPair.access_type === "private");
<div className="flex">
<SearchMultiSelectDropdown
options={ccPairs
.filter(
(ccPair) =>
!selectedCCPairIds.includes(ccPair.cc_pair_id) &&
!userGroup.cc_pairs
.map((userGroupCCPair) => userGroupCCPair.id)
.includes(ccPair.cc_pair_id)
)
// remove public and synced docs, since they don't make sense as part of a group
.filter((ccPair) => ccPair.access_type === "private")
.map((ccPair) => {
return {
name: ccPair.name?.toString() || "",
value: ccPair.cc_pair_id?.toString(),
metadata: {
ccPairId: ccPair.cc_pair_id,
connector: ccPair.connector,
},
};
})}
onSelect={(option) => {
setSelectedCCPairIds([
...Array.from(
new Set([
...selectedCCPairIds,
parseInt(option.value as string),
])
),
]);
}}
itemComponent={({ option }) => (
<div className="flex px-4 py-2.5 hover:bg-accent-background-hovered cursor-pointer">
<div className="my-auto">
<ConnectorTitle
ccPairId={option?.metadata?.ccPairId as number}
ccPairName={option.name}
connector={option?.metadata?.connector as Connector<any>}
isLink={false}
showMetadata={false}
/>
</div>
<div className="ml-auto my-auto">
<FiPlus />
</div>
</div>
)}
/>
<Button
className="ml-3 flex-nowrap w-48"
onClick={async () => {
const newCCPairIds = [
...Array.from(
new Set(
userGroup.cc_pairs
.map((ccPair) => ccPair.id)
.concat(selectedCCPairIds)
)
),
];
const response = await updateUserGroup(userGroup.id, {
user_ids: userGroup.users.map((user) => user.id),
cc_pair_ids: newCCPairIds,
return (
<Modal
className="max-w-3xl"
title="Add New Connector"
onOutsideClick={() => onClose()}
>
<div className="px-6 pt-4">
<ConnectorMultiSelect
name="connectors"
label="Select Connectors"
connectors={availableCCPairs}
selectedIds={selectedCCPairIds}
onChange={setSelectedCCPairIds}
placeholder="Search for connectors to add..."
showError={false}
/>
<Button
className="mt-4 flex-nowrap w-48"
onClick={async () => {
const newCCPairIds = [
...Array.from(
new Set(
userGroup.cc_pairs
.map((ccPair) => ccPair.id)
.concat(selectedCCPairIds)
)
),
];
const response = await updateUserGroup(userGroup.id, {
user_ids: userGroup.users.map((user) => user.id),
cc_pair_ids: newCCPairIds,
});
if (response.ok) {
setPopup({
message: "Successfully added connectors to group",
type: "success",
});
if (response.ok) {
setPopup({
message: "Successfully added users to group",
type: "success",
});
onClose();
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
setPopup({
message: `Failed to add users to group - ${errorMsg}`,
type: "error",
});
onClose();
}
}}
>
Add Connectors
</Button>
</div>
onClose();
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
setPopup({
message: `Failed to add connectors to group - ${errorMsg}`,
type: "error",
});
onClose();
}
}}
>
Add Connectors
</Button>
</div>
</Modal>
);

View File

@@ -0,0 +1,232 @@
import React, { useState, useRef, useEffect } from "react";
import { ConnectorStatus } from "@/lib/types";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { X, Search } from "lucide-react";
import { Label } from "@/components/ui/label";
import { ErrorMessage } from "formik";
interface ConnectorMultiSelectProps {
name: string;
label: string;
connectors: ConnectorStatus<any, any>[];
selectedIds: number[];
onChange: (selectedIds: number[]) => void;
disabled?: boolean;
placeholder?: string;
showError?: boolean;
}
export const ConnectorMultiSelect = ({
name,
label,
connectors,
selectedIds,
onChange,
disabled = false,
placeholder = "Search connectors...",
showError = false,
}: ConnectorMultiSelectProps) => {
const [open, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const dropdownRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const selectedConnectors = connectors.filter((connector) =>
selectedIds.includes(connector.cc_pair_id)
);
const unselectedConnectors = connectors.filter(
(connector) => !selectedIds.includes(connector.cc_pair_id)
);
const allConnectorsSelected = unselectedConnectors.length === 0;
const filteredUnselectedConnectors = unselectedConnectors.filter(
(connector) => {
const connectorName = connector.name || connector.connector.source;
return connectorName.toLowerCase().includes(searchQuery.toLowerCase());
}
);
useEffect(() => {
if (allConnectorsSelected && open) {
setOpen(false);
inputRef.current?.blur();
setSearchQuery("");
}
}, [allConnectorsSelected, open]);
useEffect(() => {
if (allConnectorsSelected) {
inputRef.current?.blur();
setSearchQuery("");
}
}, [allConnectorsSelected, selectedIds]);
const selectConnector = (connectorId: number) => {
const newSelectedIds = [...selectedIds, connectorId];
onChange(newSelectedIds);
setSearchQuery("");
const willAllBeSelected = connectors.length === newSelectedIds.length;
if (!willAllBeSelected) {
setTimeout(() => {
inputRef.current?.focus();
}, 0);
}
};
const removeConnector = (connectorId: number) => {
onChange(selectedIds.filter((id) => id !== connectorId));
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
inputRef.current !== event.target &&
!inputRef.current?.contains(event.target as Node)
) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
setOpen(false);
}
};
const effectivePlaceholder = allConnectorsSelected
? "All connectors selected"
: placeholder;
const isInputDisabled = disabled || allConnectorsSelected;
return (
<div className="flex flex-col w-full space-y-2 mb-4">
{label && <Label className="text-base font-medium">{label}</Label>}
<p className="text-xs text-neutral-500 ">
All documents indexed by the selected connectors will be part of this
document set.
</p>
<div className="relative">
<div
className={`flex items-center border border-input rounded-md border border-neutral-200 ${
allConnectorsSelected ? "bg-neutral-50" : ""
} focus-within:ring-1 focus-within:ring-ring focus-within:border-neutral-400 transition-colors`}
>
<Search className="absolute left-3 h-4 w-4 text-neutral-500" />
<input
ref={inputRef}
type="text"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setOpen(true);
}}
onFocus={() => {
if (!allConnectorsSelected) {
setOpen(true);
}
}}
onKeyDown={handleKeyDown}
placeholder={effectivePlaceholder}
className={`h-9 w-full pl-9 pr-10 py-2 bg-transparent text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
allConnectorsSelected ? "text-neutral-500" : ""
}`}
disabled={isInputDisabled}
/>
</div>
{open && !allConnectorsSelected && (
<div
ref={dropdownRef}
className="absolute z-50 w-full mt-1 rounded-md border border-neutral-200 bg-white shadow-md default-scrollbar max-h-[300px] overflow-auto"
>
{filteredUnselectedConnectors.length === 0 ? (
<div className="py-4 text-center text-xs text-neutral-500">
{searchQuery
? "No matching connectors found"
: "No more connectors available"}
</div>
) : (
<div>
{filteredUnselectedConnectors.map((connector) => (
<div
key={connector.cc_pair_id}
className="flex items-center justify-between py-2 px-3 cursor-pointer hover:bg-neutral-50 text-xs"
onClick={() => selectConnector(connector.cc_pair_id)}
>
<div className="flex items-center truncate mr-2">
<ConnectorTitle
connector={connector.connector}
ccPairId={connector.cc_pair_id}
ccPairName={connector.name}
isLink={false}
showMetadata={false}
/>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
{selectedConnectors.length > 0 ? (
<div className="mt-3 ">
<div className="flex flex-wrap gap-1.5">
{selectedConnectors.map((connector) => (
<div
key={connector.cc_pair_id}
className="flex items-center bg-white rounded-md border border-neutral-300 transition-all px-2 py-1 max-w-full group text-xs"
>
<div className="flex items-center overflow-hidden">
<div className="flex-shrink-0 text-xs">
<ConnectorTitle
connector={connector.connector}
ccPairId={connector.cc_pair_id}
ccPairName={connector.name}
isLink={false}
showMetadata={false}
/>
</div>
</div>
<button
className="ml-1 flex-shrink-0 rounded-full w-4 h-4 flex items-center justify-center bg-neutral-100 text-neutral-500 hover:bg-neutral-200 hover:text-neutral-700 transition-colors group-hover:bg-neutral-200"
onClick={() => removeConnector(connector.cc_pair_id)}
aria-label="Remove connector"
>
<X className="h-2.5 w-2.5" />
</button>
</div>
))}
</div>
</div>
) : (
<div className="mt-3 p-3 border border-dashed border-neutral-300 rounded-md bg-neutral-50 text-neutral-500 text-xs">
No connectors selected. Search and select connectors above.
</div>
)}
{showError && (
<ErrorMessage
name={name}
component="div"
className="text-red-500 text-xs mt-1"
/>
)}
</div>
);
};

View File

@@ -0,0 +1,52 @@
import React from "react";
import { ConnectorStatus } from "@/lib/types";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { Label } from "@/components/ui/label";
import { LockIcon } from "lucide-react";
interface NonSelectableConnectorsProps {
connectors: ConnectorStatus<any, any>[];
title: string;
description: string;
}
export const NonSelectableConnectors = ({
connectors,
title,
description,
}: NonSelectableConnectorsProps) => {
if (connectors.length === 0) {
return null;
}
return (
<div className="mt-6 mb-4">
<Label className="text-base font-medium mb-1">{title}</Label>
<p className="text-xs text-neutral-500 mb-3">{description}</p>
<div className="p-3 border border-dashed border-neutral-300 rounded-md bg-neutral-50">
<div className="text-xs font-medium text-neutral-700 mb-2 flex items-center">
<LockIcon className="h-3.5 w-3.5 mr-1.5 text-neutral-500" />
Unavailable connectors:
</div>
<div className="flex flex-wrap gap-1.5">
{connectors.map((connector) => (
<div
key={`${connector.connector.id}-${connector.credential.id}`}
className="flex items-center px-2 py-1 cursor-not-allowed opacity-80 bg-white border border-neutral-300 rounded-md text-xs"
>
<div className="flex items-center max-w-[200px] text-xs">
<ConnectorTitle
connector={connector.connector}
ccPairId={connector.cc_pair_id}
ccPairName={connector.name}
isLink={false}
showMetadata={false}
/>
</div>
</div>
))}
</div>
</div>
</div>
);
};

View File

@@ -20,6 +20,7 @@ interface ConnectorTitleProps {
owner?: string;
isLink?: boolean;
showMetadata?: boolean;
className?: string;
}
export const ConnectorTitle = ({
@@ -30,6 +31,7 @@ export const ConnectorTitle = ({
isPublic = true,
isLink = true,
showMetadata = true,
className = "",
}: ConnectorTitleProps) => {
const sourceMetadata = getSourceMetadata(connector.source);
@@ -88,17 +90,17 @@ export const ConnectorTitle = ({
);
}
const mainSectionClassName = "text-blue-500 dark:text-blue-100 flex w-fit";
const mainSectionClassName = `text-blue-500 dark:text-blue-100 flex w-fit ${className}`;
const mainDisplay = (
<>
{sourceMetadata.icon({ size: 20 })}
<div className="ml-1 my-auto">
{sourceMetadata.icon({ size: 16 })}
<div className="ml-1 my-auto text-xs font-medium truncate">
{ccPairName || sourceMetadata.displayName}
</div>
</>
);
return (
<div className="my-auto">
<div className="my-auto max-w-full">
{isLink ? (
<Link
className={mainSectionClassName}
@@ -110,10 +112,10 @@ export const ConnectorTitle = ({
<div className={mainSectionClassName}>{mainDisplay}</div>
)}
{showMetadata && additionalMetadata.size > 0 && (
<div className="text-xs mt-1">
<div className="text-[10px] mt-0.5 text-gray-600 dark:text-gray-400">
{Array.from(additionalMetadata.entries()).map(([key, value]) => {
return (
<div key={key}>
<div key={key} className="truncate">
<i>{key}:</i> {value}
</div>
);

View File

@@ -3278,18 +3278,25 @@ export const CirclingArrowIcon = ({
</g>
</g>
</svg>
// <svg
// style={{ width: `${size}px`, height: `${size}px` }}
// className={`w-[${size}px] h-[${size}px] ` + className}
// viewBox="0 0 112.62 120.72"
// data-name="Layer 1"
// xmlns="http://www.w3.org/2000/svg"
// >
// <path
// strokeWidth={100}
// d="M11.64,100.12l-.4-.47-1.06,8.63a5.08,5.08,0,0,1-1.92,3.41A5.11,5.11,0,0,1,0,107L2.79,84.65v-.07a3.28,3.28,0,0,1,.08-.41h0A5.09,5.09,0,0,1,9,80.39q11.22,2.53,22.42,5.15a5,5,0,0,1,3.17,2.25,5.14,5.14,0,0,1,.64,3.84v0a5,5,0,0,1-2.25,3.16,5.08,5.08,0,0,1-3.83.65c-3.31-.75-6.62-1.52-9.92-2.28a40.71,40.71,0,0,0,2.84,3,50.09,50.09,0,0,0,26.23,13.49,48.67,48.67,0,0,0,14.71.34A47.35,47.35,0,0,0,77,106h0q2.52-1.19,4.83-2.54c1.56-.93,3.07-1.92,4.51-3a50.8,50.8,0,0,0,8.56-7.88,48.92,48.92,0,0,0,6.39-9.45l.56-1.1,10,2.69-.8,1.66a58.64,58.64,0,0,1-7.9,12.24,61.28,61.28,0,0,1-10.81,10.1c-1.68,1.23-3.46,2.4-5.32,3.5s-3.73,2.07-5.74,3a58,58,0,0,1-17,5,58.56,58.56,0,0,1-17.79-.39,60.21,60.21,0,0,1-31.58-16.26c-1.2-1.16-2.26-2.31-3.24-3.45ZM101,20.6l.4.47,1-8.63a5.11,5.11,0,1,1,10.14,1.26l-2.74,22.37,0,.07c0,.13,0,.27-.07.41h0a5.09,5.09,0,0,1-6.08,3.78c-7.47-1.69-15-3.4-22.42-5.15a5,5,0,0,1-3.16-2.25,5.1,5.1,0,0,1-.65-3.84v0a5,5,0,0,1,2.25-3.16,5.1,5.1,0,0,1,3.84-.65c3.31.75,6.61,1.52,9.92,2.28-.84-1-1.77-2-2.84-3.05a50.09,50.09,0,0,0-12.13-8.73A49.49,49.49,0,0,0,64.37,11a48.6,48.6,0,0,0-14.7-.34,47.26,47.26,0,0,0-14,4.1h0q-2.53,1.18-4.83,2.54c-1.57.93-3.07,1.92-4.52,3a50.34,50.34,0,0,0-8.55,7.88,48,48,0,0,0-6.39,9.45l-.57,1.1L.76,36l.8-1.66A58.9,58.9,0,0,1,9.46,22.1,61.63,61.63,0,0,1,20.27,12q2.54-1.85,5.32-3.5c1.81-1.06,3.73-2.07,5.74-3a58,58,0,0,1,17-5A58.56,58.56,0,0,1,66.16.89a59.77,59.77,0,0,1,17,5.74A60.4,60.4,0,0,1,97.75,17.15c1.19,1.16,2.26,2.31,3.24,3.45Z"
// />
// </svg>
);
};
export const SortIcon = ({
size = 24,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M22 18.605a.75.75 0 0 1-.75.75h-5.1a2.93 2.93 0 0 1-5.66 0H2.75a.75.75 0 1 1 0-1.5h7.74a2.93 2.93 0 0 1 5.66 0h5.1a.75.75 0 0 1 .75.75m0-13.21a.75.75 0 0 1-.75.75H18.8a2.93 2.93 0 0 1-5.66 0H2.75a.75.75 0 1 1 0-1.5h10.39a2.93 2.93 0 0 1 5.66 0h2.45a.74.74 0 0 1 .75.75m0 6.6a.74.74 0 0 1-.75.75H9.55a2.93 2.93 0 0 1-5.66 0H2.75a.75.75 0 1 1 0-1.5h1.14a2.93 2.93 0 0 1 5.66 0h11.7a.75.75 0 0 1 .75.75"
/>
</svg>
);
};

View File

@@ -0,0 +1,153 @@
"use client";
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-neutral-950 dark:bg-neutral-950 dark:text-neutral-50",
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-neutral-500 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 dark:[&_[cmdk-group-heading]]:text-neutral-400">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-neutral-500 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-neutral-400",
className
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-neutral-950 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-neutral-500 dark:text-neutral-50 dark:[&_[cmdk-group-heading]]:text-neutral-400",
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-neutral-200 dark:bg-neutral-800", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-neutral-100 data-[selected=true]:text-neutral-900 data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:data-[selected='true']:bg-neutral-800 dark:data-[selected=true]:text-neutral-50",
className
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-neutral-500 dark:text-neutral-400",
className
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};