Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix connection delete and change table actions UX #45

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/actions/query-keyspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { actionClient } from "@scylla-studio/lib/safe-actions";
import { z } from "zod";

import { Auth, Cluster, ClusterConfig } from "@lambda-group/scylladb";
import { type Auth, Cluster } from "@lambda-group/scylladb";
import { parseKeyspaces } from "@scylla-studio/lib/cql-parser/parser";

export const queryKeyspaceAction = actionClient
Expand Down
150 changes: 84 additions & 66 deletions src/app/(main)/connections/_components/connection-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ContextMenuItem,
ContextMenuTrigger,
} from "@scylla-studio/components/ui/context-menu";
import { Skeleton } from "@scylla-studio/components/ui/skeleton";
import {
Table,
TableBody,
Expand All @@ -19,61 +20,63 @@ import {
TableHeader,
TableRow,
} from "@scylla-studio/components/ui/table";
import { useConnectionsStore } from "@scylla-studio/hooks/use-connections";
import type { Connection } from "@scylla-studio/lib/internal-db/connections";
import { useEffect, useState, useTransition } from "react";
import {
deleteConnection,
fetchConnections,
saveNewConnection,
updateConnection,
} from "../actions/connections";
import { useEffect, useTransition } from "react";
import { toast } from "sonner";
import NewConnectionModal from "./modal";
import { RowActions } from "./row-actions";
import TableLabel from "./table-item";

export default function ConnectionTableServer() {
const [connections, setConnections] = useState<Connection[]>([]);
const [selectedConnection, setSelectedConnection] =
useState<Connection | null>(null);
const {
availableConnections,
loadingConnections,
currentConnection,
getConnections,
deleteConnection,
refreshConnection,
saveConnection,
setCurrentConnection,
updateConnection,
} = useConnectionsStore();

const [_, startTransition] = useTransition();

useEffect(() => {
startTransition(async () => {
const initialConnections = await fetchConnections();
setConnections(initialConnections);
await getConnections();
});
}, []);

const handleSave = async (newConnection: Connection) => {
startTransition(async () => {
await (selectedConnection && selectedConnection?.id
? updateConnection(selectedConnection.id, newConnection)
: saveNewConnection(newConnection));
const updatedConnections = await fetchConnections();
setConnections(updatedConnections);
setSelectedConnection(null);
});
return currentConnection && currentConnection?.id
? await updateConnection(currentConnection.id, newConnection)
: await saveConnection(newConnection);
};

const handleDelete = async (conn: Connection) => {
if (conn?.id) {
startTransition(async () => {
await deleteConnection(conn.id!);
const updatedConnections = await fetchConnections();
setConnections(updatedConnections);
});
try {
await deleteConnection(conn);
toast.success("Connection deleted successfully");
} catch (error) {
console.error("[ConnectionTableServer.handleDelete]", error);
toast.error("Error deleting connection, please try again later");
}
};

const handleRefresh = async (conn: Connection) => {
startTransition(async () => {
await updateConnection(conn.id!, conn);
const updatedConnection = await fetchConnections();
setConnections(updatedConnection);
});
try {
await refreshConnection(conn);
toast.success("Connection refreshed successfully");
} catch (error) {
console.error("[ConnectionTableServer.handleRefresh]", error);
toast.error("Error refreshing connection, please try again later");
}
};

const handleUpdateClick = (conn: Connection) => {
setSelectedConnection(conn);
setCurrentConnection(conn);
};

return (
Expand All @@ -93,47 +96,62 @@ export default function ConnectionTableServer() {
<TableHead>Password</TableHead>
<TableHead>DC</TableHead>
<TableHead>Nodes</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{connections.map((conn) => (
<TableRow key={conn.name}>
{[
"status",
"name",
"host",
"port",
"username",
"password",
"dc",
"nodes",
].map((key) => (
<ContextMenu key={`${conn.name}-${key}`}>
<ContextMenuTrigger asChild>
<TableCell>
<TableLabel itemKey={key} conn={conn} />
</TableCell>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onSelect={() => handleUpdateClick(conn)}>
Update
</ContextMenuItem>
<ContextMenuItem onSelect={() => handleRefresh(conn)}>
Refresh
</ContextMenuItem>
<ContextMenuItem onSelect={() => handleDelete(conn)}>
Delete
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
))}
</TableRow>
))}
{!loadingConnections &&
availableConnections?.map((conn) => (
<TableRow key={conn.name}>
{[
"status",
"name",
"host",
"port",
"username",
"password",
"dc",
"nodes",
"actions",
].map((key) => (
<ContextMenu key={`${conn.name}-${key}`}>
<ContextMenuTrigger asChild>
<TableCell>
{key === "actions" ? (
<RowActions connection={conn} />
) : (
<TableLabel itemKey={key} conn={conn} />
)}
</TableCell>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onSelect={() => handleUpdateClick(conn)}
>
Update
</ContextMenuItem>
<ContextMenuItem onSelect={() => handleRefresh(conn)}>
Refresh
</ContextMenuItem>
<ContextMenuItem onSelect={() => handleDelete(conn)}>
Delete
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
))}
</TableRow>
))}
</TableBody>
</Table>
{loadingConnections && (
<div className="flex flex-col gap-1 mt-1">
<Skeleton className="flex h-9" />
<Skeleton className="flex h-9" />
</div>
)}
<NewConnectionModal
onSave={handleSave}
connectionToEdit={selectedConnection}
connectionToEdit={currentConnection}
/>
</CardContent>
</Card>
Expand Down
16 changes: 10 additions & 6 deletions src/app/(main)/connections/_components/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { Input } from "@scylla-studio/components/ui/input";
import { Modal } from "@scylla-studio/components/ui/modal";
import { useLayout } from "@scylla-studio/contexts/layout";
import { Plus } from "lucide-react";
import { Loader2, Plus } from "lucide-react";
import { type ReactNode, useEffect, useState, useTransition } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { z } from "zod";
Expand Down Expand Up @@ -81,15 +81,13 @@ export default function NewConnectionModal({
const [isPending, startTransition] = useTransition();

useEffect(() => {
if (connectionToEdit) {
setOpen(true);
}
if (connectionToEdit) setOpen(true);
}, [connectionToEdit]);

const handleSave = (data: z.infer<typeof formSchema>) => {
startTransition(async () => {
await onSave(data);
fetchInitialConnections();
await fetchInitialConnections();
setOpen(false);
});
};
Expand Down Expand Up @@ -178,7 +176,13 @@ export default function NewConnectionModal({
/>

<Button type="submit" disabled={isPending}>
{connectionToEdit ? "Update Connection" : "Save Connection"}
{isPending ? (
<Loader2 className="animate-spin" />
) : connectionToEdit ? (
"Update Connection"
) : (
"Save Connection"
)}
</Button>
</div>
</FormWrapper>
Expand Down
66 changes: 66 additions & 0 deletions src/app/(main)/connections/_components/row-actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@scylla-studio/components/ui/dropdown-menu";
import { useConnectionsStore } from "@scylla-studio/hooks/use-connections";
import type { Connection } from "@scylla-studio/lib/internal-db/connections";
import { EditIcon } from "lucide-react";
import { toast } from "sonner";

export const RowActions = ({
connection: conn,
}: { connection: Connection }) => {
const { deleteConnection, refreshConnection, setCurrentConnection } =
useConnectionsStore();

const handleDelete = async () => {
try {
await deleteConnection(conn);
toast.success("Connection deleted successfully");
} catch (error) {
console.error("[ConnectionTableServer.handleDelete]", error);
toast.error("Error deleting connection, please try again later");
}
};

const handleRefresh = async () => {
try {
await refreshConnection(conn);
toast.success("Connection refreshed successfully");
} catch (error) {
console.error("[ConnectionTableServer.handleRefresh]", error);
toast.error("Error refreshing connection, please try again later");
}
};

const handleUpdate = () => {
setCurrentConnection(conn);
};

return (
<DropdownMenu>
<DropdownMenuTrigger>
<EditIcon size={16} className="cursor-pointer" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleUpdate} className="cursor-pointer">
Update
</DropdownMenuItem>
<DropdownMenuItem onClick={handleRefresh} className="cursor-pointer">
Refresh
</DropdownMenuItem>
<DropdownMenuSeparator />
{/* styles to keep consistent in both themes */}
<DropdownMenuItem
onClick={handleDelete}
className="bg-red-600 focus:bg-red-500 focus:text-white text-white transition-all duration-300 cursor-pointer"
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
3 changes: 1 addition & 2 deletions src/app/(main)/connections/_components/table-item.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Badge } from "@scylla-studio/components/ui/badge";
import { Label } from "@scylla-studio/components/ui/label";
import { Connection } from "@scylla-studio/lib/internal-db/connections";
import React from "react";
import type { Connection } from "@scylla-studio/lib/internal-db/connections";

export default function TableLabel({
itemKey,
Expand Down
17 changes: 14 additions & 3 deletions src/app/(main)/query-runner/_components/cql-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from "@scylla-studio/components/ui/resizable";
import { Skeleton } from "@scylla-studio/components/ui/skeleton";
import { Tabs, TabsList, TabsTrigger } from "@scylla-studio/components/ui/tabs";
import { useCqlFilters } from "@scylla-studio/hooks/use-cql-filters";
import type { AvailableConnections } from "@scylla-studio/lib/connections";
Expand Down Expand Up @@ -43,6 +44,7 @@ enum DisplayTabs {

export function CqlEditor() {
const [code, setCode] = useState("");
const [loadingResults, setLoadingResults] = useState(false);
const [queryResult, setQueryResult] = useState<
Array<Record<string, unknown>>
>([]);
Expand Down Expand Up @@ -88,7 +90,7 @@ export function CqlEditor() {
fetchSizeReference.current = fetchSize ?? null;
}, [fetchSize]);

const executeQuery = (query: string) => {
const executeQuery = async (query: string) => {
if (!currentConnectionReference.current) {
toast.error("No connection selected");
return;
Expand All @@ -105,11 +107,13 @@ export function CqlEditor() {
password: password ?? null,
};

queryExecutor.execute({
setLoadingResults(true);
await queryExecutor.executeAsync({
query,
connection,
limit,
});
setLoadingResults(false);
};

// Load saved query from localStorage when the component mounts
Expand Down Expand Up @@ -329,7 +333,14 @@ export function CqlEditor() {
</TabsList>
</Tabs>
)}
{renderTabs[activeTab as DisplayTabs]}
{loadingResults ? (
<div className="flex flex-col w-full h-full gap-1 p-1">
<Skeleton className="h-11 w-full" />
<Skeleton className="h-full w-full" />
</div>
) : (
renderTabs[activeTab as DisplayTabs]
)}
</ResizablePanel>
</ResizablePanelGroup>
</div>
Expand Down
15 changes: 15 additions & 0 deletions src/components/ui/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { cn } from "@scylla-studio/lib/utils";

function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
);
}

export { Skeleton };
Loading