fix(ui): hide the Create Vector Store flow from non proxy admins (#40148)

* fix(ui): hide the Create Vector Store flow from non proxy admins

The vector stores page rendered the Create Vector Store tab, the
+ Add Vector Store button and a GET /credentials call for every role,
while the proxy only lets proxy admins call POST /vector_store/new and
GET /credentials. Internal users landed on the create form and got an
Only proxy admin error toast. Gate all three on isProxyAdminRole and
default everyone else to the Manage tab, matching the Indexes tab and
the Add Model gating.

Resolves LIT-7131

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): exclude view-only admin sessions from the vector store create flow

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-09-07 12:23:47 -07:00 committed by GitHub
parent eea4c860f7
commit cc287a7d8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 70 additions and 22 deletions

View file

@ -45,7 +45,7 @@ describe("VectorStoreManagement loading state", () => {
it("should resolve the loading state when accessToken is null instead of showing the skeleton forever", async () => {
const user = userEvent.setup();
render(<VectorStoreManagement accessToken={null} userID={null} userRole={null} />);
render(<VectorStoreManagement accessToken={null} userID={null} userRole={null} isViewOnly={false} />);
await openManageTab(user);
expect(await screen.findByText("table-loaded")).toBeInTheDocument();
expect(mockVectorStoreListCall).not.toHaveBeenCalled();
@ -59,7 +59,7 @@ describe("VectorStoreManagement loading state", () => {
resolveFetch = resolve;
}),
);
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" />);
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" isViewOnly={false} />);
await openManageTab(user);
expect(screen.getByText("table-loading")).toBeInTheDocument();
@ -69,6 +69,43 @@ describe("VectorStoreManagement loading state", () => {
});
});
describe("VectorStoreManagement create flow visibility", () => {
beforeEach(() => {
vi.clearAllMocks();
mockVectorStoreListCall.mockResolvedValue({ data: [] });
mockCredentialListCall.mockResolvedValue({ credentials: [] });
});
it.each([
{ label: "Internal User", userRole: "Internal User", isViewOnly: false },
{ label: "Internal Viewer", userRole: "Internal Viewer", isViewOnly: true },
{ label: "proxy_admin_viewer session (userRole Admin, isViewOnly)", userRole: "Admin", isViewOnly: true },
{ label: "Org Admin", userRole: "Org Admin", isViewOnly: false },
])(
"should hide the Create Vector Store tab and button and skip /credentials for $label",
async ({ userRole, isViewOnly }) => {
render(
<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole={userRole} isViewOnly={isViewOnly} />,
);
await waitFor(() => expect(mockVectorStoreListCall).toHaveBeenCalledWith("sk-test"));
expect(screen.queryByRole("tab", { name: "Create Vector Store" })).not.toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Manage Vector Stores" })).toHaveAttribute("aria-selected", "true");
expect(await screen.findByText("table-loaded")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "+ Add Vector Store" })).not.toBeInTheDocument();
expect(mockCredentialListCall).not.toHaveBeenCalled();
},
);
it("should keep the Create Vector Store tab and button and fetch /credentials for a proxy admin", async () => {
const user = userEvent.setup();
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" isViewOnly={false} />);
await waitFor(() => expect(mockCredentialListCall).toHaveBeenCalledWith("sk-test"));
expect(screen.getByRole("tab", { name: "Create Vector Store" })).toHaveAttribute("aria-selected", "true");
await openManageTab(user);
expect(screen.getByRole("button", { name: "+ Add Vector Store" })).toBeInTheDocument();
});
});
describe("VectorStoreManagement Indexes tab", () => {
beforeEach(() => {
vi.clearAllMocks();
@ -88,7 +125,7 @@ describe("VectorStoreManagement Indexes tab", () => {
},
],
});
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" />);
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" isViewOnly={false} />);
await user.click(screen.getByRole("tab", { name: "Indexes" }));
expect(await screen.findByText("support-docs-index")).toBeInTheDocument();
expect(screen.getByText("support-docs-store")).toBeInTheDocument();
@ -96,7 +133,7 @@ describe("VectorStoreManagement Indexes tab", () => {
});
it("should not render the Indexes tab for an Admin Viewer", async () => {
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin Viewer" />);
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin Viewer" isViewOnly={true} />);
await waitFor(() => expect(mockVectorStoreListCall).toHaveBeenCalledWith("sk-test"));
expect(screen.getByRole("tab", { name: "Manage Vector Stores" })).toBeInTheDocument();
expect(screen.queryByRole("tab", { name: "Indexes" })).not.toBeInTheDocument();
@ -125,7 +162,7 @@ describe("VectorStoreManagement Indexes tab", () => {
},
],
});
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" />);
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" isViewOnly={false} />);
await user.click(screen.getByRole("tab", { name: "Indexes" }));
await user.click(await screen.findByRole("button", { name: "support-docs-store" }));
expect(await screen.findByTestId("vector-store-info-view")).toHaveTextContent("vs-1");
@ -135,7 +172,7 @@ describe("VectorStoreManagement Indexes tab", () => {
it("should link to the feature docs and a GitHub issue for unsupported providers on the Indexes tab", async () => {
const user = userEvent.setup();
mockIndexesListCall.mockResolvedValue({ object: "list", data: [] });
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" />);
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" isViewOnly={false} />);
await user.click(screen.getByRole("tab", { name: "Indexes" }));
expect(screen.getByRole("link", { name: "vector store index docs" })).toHaveAttribute(
"href",
@ -149,7 +186,7 @@ describe("VectorStoreManagement Indexes tab", () => {
});
it("should not call indexesListCall until the Indexes tab is clicked", async () => {
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" />);
render(<VectorStoreManagement accessToken="sk-test" userID="user-1" userRole="Admin" isViewOnly={false} />);
await waitFor(() => expect(mockVectorStoreListCall).toHaveBeenCalledWith("sk-test"));
expect(screen.getByRole("tab", { name: "Indexes" })).toBeInTheDocument();
expect(mockIndexesListCall).not.toHaveBeenCalled();

View file

@ -24,9 +24,10 @@ interface VectorStoreProps {
accessToken: string | null;
userID: string | null;
userRole: string | null;
isViewOnly: boolean;
}
const VectorStoreManagement: React.FC<VectorStoreProps> = ({ accessToken, userID, userRole }) => {
const VectorStoreManagement: React.FC<VectorStoreProps> = ({ accessToken, userID, userRole, isViewOnly }) => {
const [vectorStores, setVectorStores] = useState<VectorStore[]>([]);
const [isLoadingVectorStores, setIsLoadingVectorStores] = useState(true);
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
@ -37,7 +38,9 @@ const VectorStoreManagement: React.FC<VectorStoreProps> = ({ accessToken, userID
const [selectedVectorStoreId, setSelectedVectorStoreId] = useState<string | null>(null);
const [editVectorStore, setEditVectorStore] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const { onTabChange, hasVisited } = useVisitedTabs("create");
const canCreateVectorStores = isProxyAdminRole(userRole || "") && !isViewOnly;
const defaultTab = canCreateVectorStores ? "create" : "manage";
const { onTabChange, hasVisited } = useVisitedTabs(defaultTab);
const fetchVectorStores = async () => {
if (!accessToken) {
@ -56,7 +59,7 @@ const VectorStoreManagement: React.FC<VectorStoreProps> = ({ accessToken, userID
};
const fetchCredentials = async () => {
if (!accessToken) return;
if (!accessToken || !canCreateVectorStores) return;
try {
const response = await credentialListCall(accessToken);
setCredentials(response.credentials || []);
@ -153,11 +156,13 @@ const VectorStoreManagement: React.FC<VectorStoreProps> = ({ accessToken, userID
You can use vector stores to store and retrieve LLM embeddings.
</p>
<Tabs defaultValue="create" onValueChange={onTabChange}>
<Tabs defaultValue={defaultTab} onValueChange={onTabChange}>
<TabsList variant="line" className="mb-6 h-auto w-full justify-start rounded-none p-0">
<TabsTrigger value="create" className="flex-none rounded-none px-4 py-2">
Create Vector Store
</TabsTrigger>
{canCreateVectorStores && (
<TabsTrigger value="create" className="flex-none rounded-none px-4 py-2">
Create Vector Store
</TabsTrigger>
)}
<TabsTrigger value="manage" className="flex-none rounded-none px-4 py-2">
Manage Vector Stores
</TabsTrigger>
@ -171,14 +176,18 @@ const VectorStoreManagement: React.FC<VectorStoreProps> = ({ accessToken, userID
)}
</TabsList>
<TabsContent keepMounted={hasVisited("create")} value="create">
<CreateVectorStore accessToken={accessToken} onSuccess={handleVectorStoreCreated} />
</TabsContent>
{canCreateVectorStores && (
<TabsContent keepMounted={hasVisited("create")} value="create">
<CreateVectorStore accessToken={accessToken} onSuccess={handleVectorStoreCreated} />
</TabsContent>
)}
<TabsContent keepMounted={hasVisited("manage")} value="manage">
<Button className="mb-4" onClick={() => setIsCreateModalVisible(true)}>
+ Add Vector Store
</Button>
{canCreateVectorStores && (
<Button className="mb-4" onClick={() => setIsCreateModalVisible(true)}>
+ Add Vector Store
</Button>
)}
<div className="grid grid-cols-1 gap-2 pt-2 pb-2 w-full mt-2">
<VectorStoreTable

View file

@ -4,6 +4,8 @@ import VectorStoreManagement from "./_components";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function VectorStores() {
const { accessToken, userRole, userId } = useAuthorized();
return <VectorStoreManagement accessToken={accessToken} userRole={userRole} userID={userId} />;
const { accessToken, userRole, userId, isViewOnly } = useAuthorized();
return (
<VectorStoreManagement accessToken={accessToken} userRole={userRole} userID={userId} isViewOnly={isViewOnly} />
);
}