mirror of
https://github.com/Mintplex-Labs/vector-admin.git
synced 2026-07-19 21:23:38 -04:00
Migration UI and job queue
This commit is contained in:
@@ -6,6 +6,7 @@ const { documentProcessorEndpoints } = require("./document-processor");
|
||||
const { documentEndpoints } = require("./documents");
|
||||
const { jobEndpoints } = require("./jobs");
|
||||
const { organizationEndpoints } = require("./organizations");
|
||||
const { toolEndpoints } = require("./tools");
|
||||
const { userEndpoints } = require("./users");
|
||||
const { workspaceEndpoints } = require("./workspaces");
|
||||
|
||||
@@ -36,6 +37,7 @@ function v1Endpoints(app) {
|
||||
documentEndpoints(app);
|
||||
documentProcessorEndpoints(app);
|
||||
jobEndpoints(app);
|
||||
toolEndpoints(app);
|
||||
}
|
||||
|
||||
module.exports = { v1Endpoints };
|
||||
|
||||
@@ -596,6 +596,7 @@ function organizationEndpoints(app) {
|
||||
documents: "countForEntity",
|
||||
vectors: "calcVectors",
|
||||
"cache-size": "calcVectorCache",
|
||||
vectorCounts: "vectorCount",
|
||||
};
|
||||
|
||||
if (!Object.keys(methods).includes(statistic)) {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
const { Organization } = require("../../../models/organization");
|
||||
const { Queue } = require("../../../models/queue");
|
||||
const {
|
||||
userFromSession,
|
||||
validSessionForUser,
|
||||
reqBody,
|
||||
} = require("../../../utils/http");
|
||||
const {
|
||||
organizationMigrationJob,
|
||||
} = require("../../../utils/jobs/organizationMigrationJob");
|
||||
|
||||
process.env.NODE_ENV === "development"
|
||||
? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
|
||||
: require("dotenv").config();
|
||||
|
||||
function toolEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
||||
app.post(
|
||||
"/v1/tools/org/:orgSlug/migrate",
|
||||
[validSessionForUser],
|
||||
async function (request, response) {
|
||||
try {
|
||||
const { orgSlug } = request.params;
|
||||
const { destinationOrgId } = reqBody(request);
|
||||
const user = await userFromSession(request);
|
||||
if (!user || user.role !== "admin") {
|
||||
response.sendStatus(403).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const organization = await Organization.getWithOwner(user.id, {
|
||||
slug: orgSlug,
|
||||
});
|
||||
if (!organization) {
|
||||
response
|
||||
.status(200)
|
||||
.json({ success: false, message: "No org found." });
|
||||
return;
|
||||
}
|
||||
|
||||
const destinationOrg = await Organization.getWithOwner(user.id, {
|
||||
id: Number(destinationOrgId),
|
||||
});
|
||||
if (!destinationOrg) {
|
||||
response
|
||||
.status(200)
|
||||
.json({
|
||||
success: false,
|
||||
message: "Destination org does not exit.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const existingJobForOrg = await Queue.get({
|
||||
taskName: "workspace/migrate",
|
||||
status: "pending",
|
||||
organization_id: organization.id,
|
||||
});
|
||||
const existingJobForDestinationOrg = await Queue.get({
|
||||
taskName: "workspace/migrate",
|
||||
status: "pending",
|
||||
organization_id: destinationOrg.id,
|
||||
});
|
||||
//TODO - uncomment if (!!existingJobForOrg || !!existingJobForDestinationOrg) {
|
||||
// response.status(200).json({ success: false, message: "There is an existing migration job already running for these organizations." });
|
||||
// return;
|
||||
// }
|
||||
|
||||
await organizationMigrationJob(organization, destinationOrg, user);
|
||||
response
|
||||
.status(200)
|
||||
.json({ success: true, message: "Migration job queued." });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { toolEndpoints };
|
||||
@@ -3,6 +3,7 @@ const path = require("path");
|
||||
const { v5 } = require("uuid");
|
||||
const { fetchMetadata } = require("../utils/storage");
|
||||
const { DocumentVectors } = require("./documentVectors");
|
||||
const { selectConnector } = require("../utils/vectordatabases/providers");
|
||||
|
||||
const WorkspaceDocument = {
|
||||
vectorFilenameRaw: function (documentName, workspaceId) {
|
||||
@@ -158,6 +159,25 @@ const WorkspaceDocument = {
|
||||
|
||||
return totalBytes;
|
||||
},
|
||||
// Will get both the remote and local count of vectors to see if the numbers match.
|
||||
vectorCount: async function (field = "organization_id", value = null) {
|
||||
try {
|
||||
const { OrganizationConnection } = require("./organizationConnection");
|
||||
const connector = await OrganizationConnection.get({ [field]: value });
|
||||
if (!connector) return { remoteCount: 0, localCount: 0 };
|
||||
const vectorDb = selectConnector(connector);
|
||||
|
||||
return {
|
||||
remoteCount: (await vectorDb.totalIndicies())?.result,
|
||||
localCount: await DocumentVectors.count({
|
||||
[field]: value,
|
||||
}),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports.WorkspaceDocument = WorkspaceDocument;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
const { Queue } = require("../../../models/queue");
|
||||
|
||||
async function organizationMigrationJob(
|
||||
organization,
|
||||
destinationOrganization,
|
||||
user
|
||||
) {
|
||||
const taskName = `organization/migrate`;
|
||||
const jobData = { organization, destinationOrganization };
|
||||
const { job, error } = await Queue.create(
|
||||
taskName,
|
||||
jobData,
|
||||
user.id,
|
||||
organization.id
|
||||
);
|
||||
if (!!error) return { job, error };
|
||||
await Queue.sendJob({
|
||||
name: taskName,
|
||||
data: {
|
||||
jobId: job.id,
|
||||
...jobData,
|
||||
},
|
||||
});
|
||||
return { job, error: null };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
organizationMigrationJob,
|
||||
};
|
||||
@@ -24,6 +24,9 @@ const OnboardingSecuritySetup = lazy(
|
||||
const OrganizationJobsView = lazy(() => import('./pages/Jobs'));
|
||||
const OrganizationToolsView = lazy(() => import('./pages/Tools'));
|
||||
const SystemSettingsView = lazy(() => import('./pages/SystemSettings'));
|
||||
const MigrateConnectionView = lazy(
|
||||
() => import('./pages/Tools/MigrateConnection')
|
||||
);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -78,7 +81,7 @@ function App() {
|
||||
|
||||
<Route
|
||||
path="/dashboard/:slug/tools/db-migration"
|
||||
element={<PrivateRoute Component={OrganizationToolsView} />}
|
||||
element={<PrivateRoute Component={MigrateConnectionView} />}
|
||||
/>
|
||||
|
||||
<Route path="/auth/sign-up" element={<SignUp />} />
|
||||
|
||||
@@ -95,8 +95,9 @@ export default function Sidebar({
|
||||
<>
|
||||
<aside
|
||||
ref={sidebar}
|
||||
className={`absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-slate-900 duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
className={`absolute left-0 top-0 z-9999 flex h-screen w-72.5 flex-col overflow-y-hidden bg-slate-900 duration-300 ease-linear dark:bg-boxdark lg:static lg:translate-x-0 ${
|
||||
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
{/* <!-- SIDEBAR HEADER --> */}
|
||||
<div className="flex items-center justify-between gap-2 px-6 py-5.5 lg:py-6.5">
|
||||
@@ -161,10 +162,11 @@ export default function Sidebar({
|
||||
<React.Fragment>
|
||||
<NavLink
|
||||
to="#"
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${(pathname === '/' ||
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
(pathname === '/' ||
|
||||
pathname.includes('dashboard')) &&
|
||||
'bg-graydark dark:bg-meta-4'
|
||||
}`}
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
sidebarExpanded
|
||||
@@ -175,14 +177,16 @@ export default function Sidebar({
|
||||
<Command className="h-4 w-4" />
|
||||
Organizations
|
||||
<ChevronUp
|
||||
className={`absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 fill-current ${open && 'rotate-180'
|
||||
}`}
|
||||
className={`absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 fill-current ${
|
||||
open && 'rotate-180'
|
||||
}`}
|
||||
/>
|
||||
</NavLink>
|
||||
{/* <!-- Dropdown Menu Start --> */}
|
||||
<div
|
||||
className={`translate transform overflow-hidden ${!open && 'hidden'
|
||||
}`}
|
||||
className={`translate transform overflow-hidden ${
|
||||
!open && 'hidden'
|
||||
}`}
|
||||
>
|
||||
<ul className="mb-5.5 mt-4 flex flex-col gap-2.5 pl-6">
|
||||
{organizations.map((org: any, i: number) => {
|
||||
@@ -225,10 +229,11 @@ export default function Sidebar({
|
||||
<React.Fragment>
|
||||
<NavLink
|
||||
to="#"
|
||||
className={`group relative flex items-center gap-2.5 rounded-t-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${(pathname === '/' ||
|
||||
className={`group relative flex items-center gap-2.5 rounded-t-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
(pathname === '/' ||
|
||||
pathname.includes('dashboard')) &&
|
||||
'bg-graydark dark:bg-meta-4'
|
||||
}`}
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
sidebarExpanded
|
||||
@@ -239,14 +244,16 @@ export default function Sidebar({
|
||||
<Box className="h-4 w-4" />
|
||||
Workspaces
|
||||
<ChevronUp
|
||||
className={`absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 fill-current ${open && 'rotate-180'
|
||||
}`}
|
||||
className={`absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 fill-current ${
|
||||
open && 'rotate-180'
|
||||
}`}
|
||||
/>
|
||||
</NavLink>
|
||||
{/* <!-- Dropdown Menu Start --> */}
|
||||
<div
|
||||
className={`translate transform overflow-hidden ${!open && 'hidden'
|
||||
}`}
|
||||
className={`translate transform overflow-hidden ${
|
||||
!open && 'hidden'
|
||||
}`}
|
||||
>
|
||||
<WorkspaceSearch
|
||||
RenderComponent={WorkspaceItem}
|
||||
@@ -306,10 +313,11 @@ export default function Sidebar({
|
||||
<div className={`translate transform overflow-hidden`}>
|
||||
<NavLink
|
||||
to={paths.toolsHome(organization)}
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${(pathname === '/' ||
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
(pathname === '/' ||
|
||||
pathname.includes('all-tools')) &&
|
||||
'bg-graydark dark:bg-meta-4'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<Package className="h-4 w-4" />
|
||||
Tools & More
|
||||
@@ -320,10 +328,11 @@ export default function Sidebar({
|
||||
<div className={`translate transform overflow-hidden`}>
|
||||
<NavLink
|
||||
to={paths.users()}
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${(pathname === '/' ||
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
(pathname === '/' ||
|
||||
pathname.includes('users')) &&
|
||||
'bg-graydark dark:bg-meta-4'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<Users className="h-4 w-4" />
|
||||
User Management
|
||||
@@ -334,12 +343,13 @@ export default function Sidebar({
|
||||
<div className={`translate transform overflow-hidden`}>
|
||||
<NavLink
|
||||
to={paths.organizationSettings(organization)}
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${(pathname === '/' ||
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
(pathname === '/' ||
|
||||
pathname.includes(
|
||||
`${organization?.slug}/settings`
|
||||
)) &&
|
||||
'bg-graydark dark:bg-meta-4'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<Briefcase className="h-4 w-4" />
|
||||
Organization Settings
|
||||
@@ -354,10 +364,11 @@ export default function Sidebar({
|
||||
<div className={`translate transform overflow-hidden`}>
|
||||
<NavLink
|
||||
to={paths.settings()}
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${(pathname === '/' ||
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
(pathname === '/' ||
|
||||
pathname.includes('system-settings')) &&
|
||||
'bg-graydark dark:bg-meta-4'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<Tool className="h-4 w-4" />
|
||||
System Settings
|
||||
@@ -370,9 +381,10 @@ export default function Sidebar({
|
||||
<div className={`translate transform overflow-hidden`}>
|
||||
<NavLink
|
||||
to={paths.jobs(organization)}
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${(pathname === '/' || pathname.includes('jobs')) &&
|
||||
className={`group relative flex items-center gap-2.5 rounded-sm px-4 py-2 font-medium text-bodydark1 duration-300 ease-in-out hover:bg-graydark dark:hover:bg-meta-4 ${
|
||||
(pathname === '/' || pathname.includes('jobs')) &&
|
||||
'bg-graydark dark:bg-meta-4'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<Radio className="h-4 w-4" />
|
||||
Background Jobs
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { API_BASE } from '../utils/constants';
|
||||
import { baseHeaders } from '../utils/request';
|
||||
|
||||
const Tools = {
|
||||
migrateOrg: async (
|
||||
slug: string,
|
||||
config = {}
|
||||
): Promise<{ success: boolean; message: null | string }> => {
|
||||
return fetch(`${API_BASE}/v1/tools/org/${slug}/migrate`, {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
body: JSON.stringify({ ...config }),
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => res)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return { success: false, message: e.message };
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default Tools;
|
||||
@@ -0,0 +1,350 @@
|
||||
import { FullScreenLoader } from '../../../components/Preloader';
|
||||
import useUser from '../../../hooks/useUser';
|
||||
import { useState, useEffect } from 'react';
|
||||
import DefaultLayout from '../../../layout/DefaultLayout';
|
||||
import User from '../../../models/user';
|
||||
import paths from '../../../utils/paths';
|
||||
import AppLayout from '../../../layout/AppLayout';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { APP_NAME } from '../../../utils/constants';
|
||||
import Organization from '../../../models/organization';
|
||||
import { CheckCircle, Info, Loader } from 'react-feather';
|
||||
import { nFormatter } from '../../../utils/numbers';
|
||||
import pluralize from 'pluralize';
|
||||
import Tools from '../../../models/tools';
|
||||
import showToast from '../../../utils/toast';
|
||||
|
||||
export default function MigrateConnectionView() {
|
||||
const { user } = useUser();
|
||||
const { slug } = useParams();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [organizations, setOrganizations] = useState<object[]>([]);
|
||||
const [organization, setOrganization] = useState<object | null>(null);
|
||||
const [totalVectorCount, setTotalVectorCount] = useState<number>(0);
|
||||
const [destination, setDestination] = useState<object | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function userOrgs() {
|
||||
if (!slug) return false;
|
||||
|
||||
const orgs = await User.organizations();
|
||||
if (orgs.length === 0) {
|
||||
window.location.replace(paths.onboarding.orgName());
|
||||
return false;
|
||||
}
|
||||
for (const org of orgs) {
|
||||
const connector = await Organization.connector(org.slug);
|
||||
org.connector = connector || {};
|
||||
}
|
||||
|
||||
const focusedOrg =
|
||||
orgs?.find((org: any) => org.slug === slug) || orgs?.[0];
|
||||
|
||||
setOrganizations(orgs);
|
||||
setOrganization(focusedOrg);
|
||||
setLoading(false);
|
||||
}
|
||||
userOrgs();
|
||||
}, [user.uid, window.location.pathname]);
|
||||
|
||||
if (loading || organizations.length === 0) {
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<FullScreenLoader />
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (!organization) return null;
|
||||
return (
|
||||
<AppLayout
|
||||
headerEntity={organization}
|
||||
headerProp="uuid"
|
||||
organizations={organizations}
|
||||
organization={organization}
|
||||
workspaces={[]}
|
||||
>
|
||||
<div className="flex-1 rounded-sm bg-white px-6 pb-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="mb-6">
|
||||
<h4 className="text-3xl font-semibold text-black">
|
||||
Migrate your vector data
|
||||
</h4>
|
||||
<p className="mt-2 text-gray-600">
|
||||
This tool will enable you to migrate all data in this vector
|
||||
database to another known {APP_NAME} vector database connection.
|
||||
<br />
|
||||
No data will be modified or removed from the current vector
|
||||
database.
|
||||
<br />
|
||||
You should only migrate to empty vector databases.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-1/2 flex-col gap-y-2">
|
||||
<div className="flex w-full justify-between ">
|
||||
<CurrentOrgCard
|
||||
organization={organization}
|
||||
onCount={setTotalVectorCount}
|
||||
/>
|
||||
<div className="flex">
|
||||
<p className="my-auto text-xl font-semibold text-gray-700">to</p>
|
||||
</div>
|
||||
<DestinationOrgCard
|
||||
currentOrg={organization}
|
||||
selection={destination}
|
||||
organizations={organizations}
|
||||
onSelect={setDestination}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!!destination && totalVectorCount > 0 && (
|
||||
<SubmitMigrationJob
|
||||
organization={organization}
|
||||
destination={destination}
|
||||
totalVectorCount={totalVectorCount}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function CurrentOrgCard({
|
||||
organization,
|
||||
onCount,
|
||||
}: {
|
||||
organization: any;
|
||||
onCount: (count: number) => void;
|
||||
}) {
|
||||
const [inSync, setInSync] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
async function OrgInSync() {
|
||||
const { remoteCount, localCount } = (
|
||||
await Organization.stats(organization.slug, 'vectorCounts')
|
||||
).value;
|
||||
if (remoteCount === 0) {
|
||||
setInSync(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setInSync(remoteCount === localCount);
|
||||
onCount(remoteCount);
|
||||
setLoading(false);
|
||||
}
|
||||
OrgInSync();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="mb-1 font-semibold text-gray-700">
|
||||
Migrating all data from
|
||||
</p>
|
||||
<div className="w-[20rem] rounded-lg border border-gray-200 bg-white p-6 shadow">
|
||||
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900">
|
||||
{organization?.name}
|
||||
</h5>
|
||||
<p className="mb-3 text-sm font-normal text-gray-700">
|
||||
Connected to a <b>{organization?.connector?.type}</b> vector database.
|
||||
</p>
|
||||
<div>
|
||||
{loading ? (
|
||||
<div className="flex h-8 w-full animate-pulse items-center justify-center gap-x-1 rounded-lg bg-gray-100 py-1 text-sm" />
|
||||
) : (
|
||||
<>
|
||||
{inSync ? (
|
||||
<div className="flex w-full items-center justify-center gap-x-1 rounded-lg bg-green-100 py-1 text-sm text-green-600">
|
||||
<CheckCircle size={18} />
|
||||
<p>Ready to migrate!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-grey-600 flex w-full flex-1 items-center justify-center gap-x-1 rounded-lg bg-gray-100 px-1 py-1 text-sm">
|
||||
<Info size={18} className="shrink-0" />
|
||||
<p className="text-center">
|
||||
Some vectors are not synced and will not migrate.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DestinationOrgCard({
|
||||
currentOrg,
|
||||
selection,
|
||||
organizations,
|
||||
onSelect,
|
||||
}: {
|
||||
currentOrg: any;
|
||||
selection?: any | null;
|
||||
organizations: any[];
|
||||
onSelect: (org: object) => void;
|
||||
}) {
|
||||
const availableDestinations = organizations.filter(
|
||||
(org) => org.slug !== currentOrg.slug && !!org.connector
|
||||
);
|
||||
const [destination, setDestination] = useState(selection || null);
|
||||
const [isEmpty, setIsEmpty] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const handleSelection = (e) => {
|
||||
const selectedDestination = availableDestinations.find(
|
||||
(org) => org.id === Number(e.target.value)
|
||||
);
|
||||
setDestination(selectedDestination);
|
||||
onSelect(selectedDestination);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function OrgIsEmpty() {
|
||||
if (!destination) return;
|
||||
const { remoteCount } = (
|
||||
await Organization.stats(destination.slug, 'vectorCounts')
|
||||
).value;
|
||||
setIsEmpty(remoteCount === 0);
|
||||
setLoading(false);
|
||||
}
|
||||
OrgIsEmpty();
|
||||
}, [destination]);
|
||||
|
||||
if (!destination) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<p className="mb-1 font-semibold text-gray-700"> </p>
|
||||
<div className="h-full w-[20rem] rounded-lg border border-gray-200 bg-white p-6 shadow">
|
||||
<select
|
||||
onChange={handleSelection}
|
||||
name="destinationOrgId"
|
||||
className="w-full rounded-lg border border-gray-100 bg-gray-50 px-4 py-2 text-2xl text-gray-800 outline-none"
|
||||
>
|
||||
<option>Select organization</option>
|
||||
{availableDestinations.map((org: any) => {
|
||||
return <option value={org.id}>{org.name}</option>;
|
||||
})}
|
||||
</select>
|
||||
<p className="mb-3 text-sm font-normal text-gray-700"></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<p className="mb-1 font-semibold text-gray-700"> </p>
|
||||
<div className="h-full w-[20rem] rounded-lg border border-gray-200 bg-white p-6 shadow">
|
||||
<select
|
||||
onChange={handleSelection}
|
||||
name="destinationOrgId"
|
||||
className="w-full rounded-lg border border-gray-100 bg-gray-50 px-4 py-2 text-2xl text-gray-800 outline-none"
|
||||
>
|
||||
<option value={destination.id}>{destination.name}</option>
|
||||
{availableDestinations
|
||||
.filter((org) => org.id !== destination.id)
|
||||
.map((org: any) => {
|
||||
return <option value={org.id}>{org.name}</option>;
|
||||
})}
|
||||
</select>
|
||||
<p className="mb-3 text-sm font-normal text-gray-700">
|
||||
Connected to a <b>{destination?.connector?.type}</b> vector database.
|
||||
</p>
|
||||
<div>
|
||||
{loading ? (
|
||||
<div className="flex h-8 w-full animate-pulse items-center justify-center gap-x-1 rounded-lg bg-gray-100 py-1 text-sm" />
|
||||
) : (
|
||||
<>
|
||||
{isEmpty ? (
|
||||
<div className="flex w-full items-center justify-center gap-x-1 rounded-lg bg-green-100 py-1 text-sm text-green-600">
|
||||
<CheckCircle size={18} />
|
||||
<p>Ready to migrate!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-grey-600 flex w-full flex-1 items-center justify-center gap-x-1 rounded-lg bg-gray-100 px-1 py-1 text-sm">
|
||||
<Info size={18} className="shrink-0" />
|
||||
<p className="text-center">
|
||||
You should only migrate to a empty vector database.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SubmitMigrationJob({
|
||||
organization,
|
||||
destination,
|
||||
totalVectorCount,
|
||||
}: {
|
||||
organization: any;
|
||||
destination: any;
|
||||
totalVectorCount: number;
|
||||
}) {
|
||||
const [status, setStatus] = useState<
|
||||
'idle' | 'loading' | 'success' | 'failed'
|
||||
>('idle');
|
||||
|
||||
async function submitMigration() {
|
||||
setStatus('loading');
|
||||
const { success, message } = await Tools.migrateOrg(organization.slug, {
|
||||
destinationOrgId: destination.id,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
setStatus('success');
|
||||
showToast('Migration has started as background job.', 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('failed');
|
||||
showToast(message as string, 'error');
|
||||
}
|
||||
|
||||
if (status === 'success') return null;
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="mt-2 flex flex-col gap-y-1">
|
||||
<button
|
||||
disabled={true}
|
||||
className="full flex items-center justify-center gap-x-1 rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white hover:bg-blue-700"
|
||||
>
|
||||
<Loader size={14} className="animate-spin" />
|
||||
Migrating {nFormatter(totalVectorCount)}{' '}
|
||||
{pluralize('vector', totalVectorCount)} to {destination.name}...
|
||||
</button>
|
||||
<p className="mx-auto w-4/5 text-center text-xs text-gray-500">
|
||||
This task will run in the background. You can view its progress in
|
||||
Background Jobs.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2 flex flex-col gap-y-1">
|
||||
<button
|
||||
onClick={submitMigration}
|
||||
className="full rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white hover:bg-blue-700"
|
||||
>
|
||||
Migrate {nFormatter(totalVectorCount)}{' '}
|
||||
{pluralize('vector', totalVectorCount)} to {destination.name}
|
||||
</button>
|
||||
<p className="mx-auto w-4/5 text-center text-xs text-gray-500">
|
||||
Depending on the number of vectors this process can take a long time.
|
||||
Please do not edit or update either vector database while the process is
|
||||
running.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -61,8 +61,7 @@ const ToolItem = ({
|
||||
}) => {
|
||||
const [show, setShow] = useState(false);
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between border-b border-gray-200 py-5 text-left text-gray-500 dark:border-gray-700 dark:text-gray-400"
|
||||
>
|
||||
<div className="flex w-full items-center justify-between border-b border-gray-200 py-5 text-left text-gray-500 dark:border-gray-700 dark:text-gray-400">
|
||||
<div className="flex w-full items-center justify-between pr-4">
|
||||
<div className="flex items-center gap-x-8">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
@@ -78,7 +77,10 @@ const ToolItem = ({
|
||||
<p className="text-sm font-normal">feature under development.</p>
|
||||
</div>
|
||||
) : (
|
||||
<a href={linkTo} className="flex items-center gap-x-1 rounded-md bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 text-slate-600 ">
|
||||
<a
|
||||
href={linkTo}
|
||||
className="flex items-center gap-x-1 rounded-md bg-blue-600 px-4 py-1.5 text-slate-600 text-white hover:bg-blue-700 "
|
||||
>
|
||||
<p className="text-sm font-normal ">Open tool →</p>
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -39,7 +39,7 @@ const paths = {
|
||||
tools: {
|
||||
migrationTool: function ({ slug }: { slug: string }) {
|
||||
return `/dashboard/${slug}/tools/db-migration`;
|
||||
}
|
||||
},
|
||||
},
|
||||
dashboard: function () {
|
||||
return '/dashboard';
|
||||
|
||||
Reference in New Issue
Block a user