mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-15 05:45:20 +00:00
connections qol improvements (#195)
* add client side polling to connections list * properly fetch repo image url * add client polling to connection management page, and add ability to sync failed connections
This commit is contained in:
parent
3be3680ee2
commit
e0d363420b
10 changed files with 257 additions and 78 deletions
|
|
@ -8,7 +8,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||||
autoDeleteStaleRepos: true,
|
autoDeleteStaleRepos: true,
|
||||||
reindexIntervalMs: 1000 * 60,
|
reindexIntervalMs: 1000 * 60,
|
||||||
resyncConnectionPollingIntervalMs: 1000,
|
resyncConnectionPollingIntervalMs: 1000,
|
||||||
reindexRepoPollingInternvalMs: 1000,
|
reindexRepoPollingIntervalMs: 1000,
|
||||||
indexConcurrencyMultiple: 3,
|
indexConcurrencyMultiple: 3,
|
||||||
configSyncConcurrencyMultiple: 3,
|
configSyncConcurrencyMultiple: 3,
|
||||||
}
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ export const compileGithubConfig = async (
|
||||||
external_codeHostUrl: hostUrl,
|
external_codeHostUrl: hostUrl,
|
||||||
cloneUrl: cloneUrl.toString(),
|
cloneUrl: cloneUrl.toString(),
|
||||||
name: repoName,
|
name: repoName,
|
||||||
|
imageUrl: repo.owner.avatar_url,
|
||||||
isFork: repo.fork,
|
isFork: repo.fork,
|
||||||
isArchived: !!repo.archived,
|
isArchived: !!repo.archived,
|
||||||
org: {
|
org: {
|
||||||
|
|
@ -80,6 +81,7 @@ export const compileGitlabConfig = async (
|
||||||
external_codeHostUrl: hostUrl,
|
external_codeHostUrl: hostUrl,
|
||||||
cloneUrl: cloneUrl.toString(),
|
cloneUrl: cloneUrl.toString(),
|
||||||
name: project.path_with_namespace,
|
name: project.path_with_namespace,
|
||||||
|
imageUrl: project.avatar_url,
|
||||||
isFork: isFork,
|
isFork: isFork,
|
||||||
isArchived: !!project.archived,
|
isArchived: !!project.archived,
|
||||||
org: {
|
org: {
|
||||||
|
|
@ -118,7 +120,6 @@ export const compileGiteaConfig = async (
|
||||||
const hostUrl = config.url ?? 'https://gitea.com';
|
const hostUrl = config.url ?? 'https://gitea.com';
|
||||||
|
|
||||||
return giteaRepos.map((repo) => {
|
return giteaRepos.map((repo) => {
|
||||||
const repoUrl = `${hostUrl}/${repo.full_name}`;
|
|
||||||
const cloneUrl = new URL(repo.clone_url!);
|
const cloneUrl = new URL(repo.clone_url!);
|
||||||
|
|
||||||
const record: RepoData = {
|
const record: RepoData = {
|
||||||
|
|
@ -127,6 +128,7 @@ export const compileGiteaConfig = async (
|
||||||
external_codeHostUrl: hostUrl,
|
external_codeHostUrl: hostUrl,
|
||||||
cloneUrl: cloneUrl.toString(),
|
cloneUrl: cloneUrl.toString(),
|
||||||
name: repo.full_name!,
|
name: repo.full_name!,
|
||||||
|
imageUrl: repo.owner?.avatar_url,
|
||||||
isFork: repo.fork!,
|
isFork: repo.fork!,
|
||||||
isArchived: !!repo.archived,
|
isArchived: !!repo.archived,
|
||||||
org: {
|
org: {
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export class RepoManager implements IRepoManager {
|
||||||
this.fetchAndScheduleRepoIndexing();
|
this.fetchAndScheduleRepoIndexing();
|
||||||
this.garbageCollectRepo();
|
this.garbageCollectRepo();
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, this.settings.reindexRepoPollingInternvalMs));
|
await new Promise(resolve => setTimeout(resolve, this.settings.reindexRepoPollingIntervalMs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export type Settings = {
|
||||||
/**
|
/**
|
||||||
* The polling rate (in milliseconds) at which the db should be checked for repos that should be re-indexed.
|
* The polling rate (in milliseconds) at which the db should be checked for repos that should be re-indexed.
|
||||||
*/
|
*/
|
||||||
reindexRepoPollingInternvalMs: number;
|
reindexRepoPollingIntervalMs: number;
|
||||||
/**
|
/**
|
||||||
* The multiple of the number of CPUs to use for indexing.
|
* The multiple of the number of CPUs to use for indexing.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema";
|
||||||
import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
|
import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
|
||||||
import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
|
import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
|
||||||
import { encrypt } from "@sourcebot/crypto"
|
import { encrypt } from "@sourcebot/crypto"
|
||||||
import { getConnection } from "./data/connection";
|
import { getConnection, getLinkedRepos } from "./data/connection";
|
||||||
import { ConnectionSyncStatus, Prisma, Invite, OrgRole } from "@sourcebot/db";
|
import { ConnectionSyncStatus, Prisma, Invite, OrgRole, Connection, Repo, Org } from "@sourcebot/db";
|
||||||
import { headers } from "next/headers"
|
import { headers } from "next/headers"
|
||||||
import { getStripe } from "@/lib/stripe"
|
import { getStripe } from "@/lib/stripe"
|
||||||
import { getUser } from "@/data/user";
|
import { getUser } from "@/data/user";
|
||||||
|
|
@ -236,6 +236,41 @@ export const createConnection = async (name: string, type: string, connectionCon
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const getConnectionInfoAction = async (connectionId: number, domain: string): Promise<{ connection: Connection, linkedRepos: Repo[] } | ServiceError> =>
|
||||||
|
withAuth((session) =>
|
||||||
|
withOrgMembership(session, domain, async (orgId) => {
|
||||||
|
const connection = await getConnection(connectionId, orgId);
|
||||||
|
if (!connection) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkedRepos = await getLinkedRepos(connectionId, orgId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
connection,
|
||||||
|
linkedRepos: linkedRepos.map((repo) => repo.repo),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getOrgFromDomainAction = async (domain: string): Promise<Org | ServiceError> =>
|
||||||
|
withAuth((session) =>
|
||||||
|
withOrgMembership(session, domain, async (orgId) => {
|
||||||
|
const org = await prisma.org.findUnique({
|
||||||
|
where: {
|
||||||
|
id: orgId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return org;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
export const updateConnectionDisplayName = async (connectionId: number, name: string, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
export const updateConnectionDisplayName = async (connectionId: number, name: string, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
||||||
withAuth((session) =>
|
withAuth((session) =>
|
||||||
withOrgMembership(session, domain, async (orgId) => {
|
withOrgMembership(session, domain, async (orgId) => {
|
||||||
|
|
@ -298,6 +333,36 @@ export const updateConnectionConfigAndScheduleSync = async (connectionId: number
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const flagConnectionForSync = async (connectionId: number, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
||||||
|
withAuth((session) =>
|
||||||
|
withOrgMembership(session, domain, async (orgId) => {
|
||||||
|
const connection = await getConnection(connectionId, orgId);
|
||||||
|
if (!connection || connection.orgId !== orgId) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection.syncStatus !== "FAILED") {
|
||||||
|
return {
|
||||||
|
statusCode: StatusCodes.BAD_REQUEST,
|
||||||
|
errorCode: ErrorCode.CONNECTION_NOT_FAILED,
|
||||||
|
message: "Connection is not in a failed state. Cannot flag for sync.",
|
||||||
|
} satisfies ServiceError;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.connection.update({
|
||||||
|
where: {
|
||||||
|
id: connection.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
syncStatus: "SYNC_NEEDED",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
export const deleteConnection = async (connectionId: number, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
export const deleteConnection = async (connectionId: number, domain: string): Promise<{ success: boolean } | ServiceError> =>
|
||||||
withAuth((session) =>
|
withAuth((session) =>
|
||||||
withOrgMembership(session, domain, async (orgId) => {
|
withOrgMembership(session, domain, async (orgId) => {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export const RepoListItem = ({
|
||||||
case RepoIndexingStatus.NEW:
|
case RepoIndexingStatus.NEW:
|
||||||
return 'Waiting...';
|
return 'Waiting...';
|
||||||
case RepoIndexingStatus.IN_INDEX_QUEUE:
|
case RepoIndexingStatus.IN_INDEX_QUEUE:
|
||||||
|
return 'In index queue...';
|
||||||
case RepoIndexingStatus.INDEXING:
|
case RepoIndexingStatus.INDEXING:
|
||||||
return 'Indexing...';
|
return 'Indexing...';
|
||||||
case RepoIndexingStatus.INDEXED:
|
case RepoIndexingStatus.INDEXED:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { NotFound } from "@/app/[domain]/components/notFound";
|
import { NotFound } from "@/app/[domain]/components/notFound";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
|
|
@ -10,58 +12,108 @@ import {
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { TabSwitcher } from "@/components/ui/tab-switcher";
|
import { TabSwitcher } from "@/components/ui/tab-switcher";
|
||||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||||
import { getConnection, getLinkedRepos } from "@/data/connection";
|
|
||||||
import { ConnectionIcon } from "../components/connectionIcon";
|
import { ConnectionIcon } from "../components/connectionIcon";
|
||||||
import { Header } from "../../components/header";
|
import { Header } from "../../components/header";
|
||||||
import { ConfigSetting } from "./components/configSetting";
|
import { ConfigSetting } from "./components/configSetting";
|
||||||
import { DeleteConnectionSetting } from "./components/deleteConnectionSetting";
|
import { DeleteConnectionSetting } from "./components/deleteConnectionSetting";
|
||||||
import { DisplayNameSetting } from "./components/displayNameSetting";
|
import { DisplayNameSetting } from "./components/displayNameSetting";
|
||||||
import { RepoListItem } from "./components/repoListItem";
|
import { RepoListItem } from "./components/repoListItem";
|
||||||
import { getOrgFromDomain } from "@/data/org";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
import { PageNotFound } from "../../components/pageNotFound";
|
import { useEffect, useState } from "react";
|
||||||
|
import { Connection, Repo, Org } from "@sourcebot/db";
|
||||||
|
import { getConnectionInfoAction, getOrgFromDomainAction, flagConnectionForSync } from "@/actions";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useToast } from "@/components/hooks/use-toast";
|
||||||
|
|
||||||
interface ConnectionManagementPageProps {
|
export default function ConnectionManagementPage() {
|
||||||
params: {
|
const params = useParams();
|
||||||
id: string;
|
const searchParams = useSearchParams();
|
||||||
domain: string;
|
const { toast } = useToast();
|
||||||
},
|
const [org, setOrg] = useState<Org | null>(null);
|
||||||
searchParams: {
|
const [connection, setConnection] = useState<Connection | null>(null);
|
||||||
tab?: string;
|
const [linkedRepos, setLinkedRepos] = useState<Repo[]>([]);
|
||||||
}
|
const [loading, setLoading] = useState(true);
|
||||||
}
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
export default async function ConnectionManagementPage({
|
useEffect(() => {
|
||||||
params,
|
const loadData = async () => {
|
||||||
searchParams,
|
try {
|
||||||
}: ConnectionManagementPageProps) {
|
const orgResult = await getOrgFromDomainAction(params.domain as string);
|
||||||
const org = await getOrgFromDomain(params.domain);
|
if (isServiceError(orgResult)) {
|
||||||
if (!org) {
|
setError(orgResult.message);
|
||||||
return <PageNotFound />
|
setLoading(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
setOrg(orgResult);
|
||||||
|
|
||||||
const connectionId = Number(params.id);
|
const connectionId = Number(params.id);
|
||||||
if (isNaN(connectionId)) {
|
if (isNaN(connectionId)) {
|
||||||
|
setError("Invalid connection ID");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionInfoResult = await getConnectionInfoAction(connectionId, params.domain as string);
|
||||||
|
if (isServiceError(connectionInfoResult)) {
|
||||||
|
setError(connectionInfoResult.message);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionInfoResult.linkedRepos.sort((a, b) => {
|
||||||
|
// Helper function to get priority of indexing status
|
||||||
|
const getPriority = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'FAILED': return 0;
|
||||||
|
case 'IN_INDEX_QUEUE':
|
||||||
|
case 'INDEXING': return 1;
|
||||||
|
case 'INDEXED': return 2;
|
||||||
|
default: return 3;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const priorityA = getPriority(a.repoIndexingStatus);
|
||||||
|
const priorityB = getPriority(b.repoIndexingStatus);
|
||||||
|
|
||||||
|
// First sort by priority
|
||||||
|
if (priorityA !== priorityB) {
|
||||||
|
return priorityA - priorityB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If same priority, sort by createdAt
|
||||||
|
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
setConnection(connectionInfoResult.connection);
|
||||||
|
setLinkedRepos(connectionInfoResult.linkedRepos);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "An error occurred while loading the connection. If the problem persists, please contact us at team@sourcebot.dev");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
const intervalId = setInterval(loadData, 1000);
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [params.domain, params.id]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !org || !connection) {
|
||||||
return (
|
return (
|
||||||
<NotFound
|
<NotFound
|
||||||
className="flex w-full h-full items-center justify-center"
|
className="flex w-full h-full items-center justify-center"
|
||||||
message="Connection not found"
|
message={error || "Not found"}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const connection = await getConnection(Number(params.id), org.id);
|
const currentTab = searchParams.get("tab") || "overview";
|
||||||
if (!connection) {
|
|
||||||
return (
|
|
||||||
<NotFound
|
|
||||||
className="flex w-full h-full items-center justify-center"
|
|
||||||
message="Connection not found"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkedRepos = await getLinkedRepos(connectionId, org.id);
|
|
||||||
|
|
||||||
const currentTab = searchParams.tab || "overview";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|
@ -116,7 +168,30 @@ export default async function ConnectionManagementPage({
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-border p-4 bg-background">
|
<div className="rounded-lg border border-border p-4 bg-background">
|
||||||
<h2 className="text-sm font-medium text-muted-foreground">Status</h2>
|
<h2 className="text-sm font-medium text-muted-foreground">Status</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<p className="mt-2 text-sm">{connection.syncStatus}</p>
|
<p className="mt-2 text-sm">{connection.syncStatus}</p>
|
||||||
|
{connection.syncStatus === "FAILED" && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2 rounded-full"
|
||||||
|
onClick={async () => {
|
||||||
|
const result = await flagConnectionForSync(connection.id, params.domain as string);
|
||||||
|
if (isServiceError(result)) {
|
||||||
|
toast({
|
||||||
|
description: `❌ Failed to flag connection for sync. Reason: ${result.message}`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
description: "✅ Connection flagged for sync.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ReloadIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -127,12 +202,12 @@ export default async function ConnectionManagementPage({
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{linkedRepos
|
{linkedRepos
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const aIndexedAt = a.repo.indexedAt ?? new Date();
|
const aIndexedAt = a.indexedAt ?? new Date();
|
||||||
const bIndexedAt = b.repo.indexedAt ?? new Date();
|
const bIndexedAt = b.indexedAt ?? new Date();
|
||||||
|
|
||||||
return bIndexedAt.getTime() - aIndexedAt.getTime();
|
return bIndexedAt.getTime() - aIndexedAt.getTime();
|
||||||
})
|
})
|
||||||
.map(({ repo }) => (
|
.map((repo) => (
|
||||||
<RepoListItem
|
<RepoListItem
|
||||||
key={repo.id}
|
key={repo.id}
|
||||||
imageUrl={repo.imageUrl ?? undefined}
|
imageUrl={repo.imageUrl ?? undefined}
|
||||||
|
|
@ -162,6 +237,5 @@ export default async function ConnectionManagementPage({
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,66 @@
|
||||||
|
"use client";
|
||||||
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
import { ConnectionListItem } from "./connectionListItem";
|
import { ConnectionListItem } from "./connectionListItem";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useState } from "react";
|
||||||
import { ConnectionSyncStatus } from "@sourcebot/db";
|
import { ConnectionSyncStatus } from "@sourcebot/db";
|
||||||
|
import { getConnections } from "@/actions";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
|
||||||
interface ConnectionListProps {
|
interface ConnectionListProps {
|
||||||
connections: {
|
|
||||||
id: number,
|
|
||||||
name: string,
|
|
||||||
connectionType: string,
|
|
||||||
syncStatus: ConnectionSyncStatus,
|
|
||||||
updatedAt: Date,
|
|
||||||
syncedAt?: Date
|
|
||||||
}[];
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectionList = ({
|
export const ConnectionList = ({
|
||||||
connections,
|
|
||||||
className,
|
className,
|
||||||
}: ConnectionListProps) => {
|
}: ConnectionListProps) => {
|
||||||
|
const domain = useDomain();
|
||||||
|
const [connections, setConnections] = useState<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
connectionType: string;
|
||||||
|
syncStatus: ConnectionSyncStatus;
|
||||||
|
updatedAt: Date;
|
||||||
|
syncedAt?: Date;
|
||||||
|
}[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchConnections = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getConnections(domain);
|
||||||
|
if (isServiceError(result)) {
|
||||||
|
setError(result.message);
|
||||||
|
} else {
|
||||||
|
setConnections(result);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'An error occured while fetching connections. If the problem persists, please contact us at team@sourcebot.dev');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchConnections();
|
||||||
|
const intervalId = setInterval(fetchConnections, 1000);
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [domain]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col gap-4", className)}>
|
<div className={cn("flex flex-col gap-4", className)}>
|
||||||
{connections.length > 0 ? connections
|
{loading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center border rounded-md p-4 h-full">
|
||||||
|
<p>Loading connections...</p>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="flex flex-col items-center justify-center border rounded-md p-4 h-full">
|
||||||
|
<p>Error loading connections: {error}</p>
|
||||||
|
</div>
|
||||||
|
) : connections.length > 0 ? (
|
||||||
|
connections
|
||||||
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
|
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
|
||||||
.map((connection) => (
|
.map((connection) => (
|
||||||
<ConnectionListItem
|
<ConnectionListItem
|
||||||
|
|
@ -36,7 +73,7 @@ export const ConnectionList = ({
|
||||||
syncedAt={connection.syncedAt ?? undefined}
|
syncedAt={connection.syncedAt ?? undefined}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center border rounded-md p-4 h-full">
|
<div className="flex flex-col items-center justify-center border rounded-md p-4 h-full">
|
||||||
<InfoCircledIcon className="w-7 h-7" />
|
<InfoCircledIcon className="w-7 h-7" />
|
||||||
<h2 className="mt-2 font-medium">No connections</h2>
|
<h2 className="mt-2 font-medium">No connections</h2>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ export default async function ConnectionsPage({ params: { domain } }: { params:
|
||||||
</Header>
|
</Header>
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<ConnectionList
|
<ConnectionList
|
||||||
connections={connections}
|
|
||||||
className="md:w-3/4"
|
className="md:w-3/4"
|
||||||
/>
|
/>
|
||||||
<NewConnectionCard
|
<NewConnectionCard
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,5 @@ export enum ErrorCode {
|
||||||
MEMBER_NOT_FOUND = 'MEMBER_NOT_FOUND',
|
MEMBER_NOT_FOUND = 'MEMBER_NOT_FOUND',
|
||||||
INVALID_CREDENTIALS = 'INVALID_CREDENTIALS',
|
INVALID_CREDENTIALS = 'INVALID_CREDENTIALS',
|
||||||
MEMBER_NOT_OWNER = 'MEMBER_NOT_OWNER',
|
MEMBER_NOT_OWNER = 'MEMBER_NOT_OWNER',
|
||||||
|
CONNECTION_NOT_FAILED = 'CONNECTION_NOT_FAILED',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue