From 564fed939f723215de50ea49d40e505e0c74d8ce Mon Sep 17 00:00:00 2001 From: Caio Ricciuti Date: Sat, 2 Nov 2024 01:15:54 +0100 Subject: [PATCH] several fix and enhancements, add alpha features --- components.json | 4 +- src/features/admin/components/UpdateUser.tsx | 125 ----- src/features/admin/components/UserTable.tsx | 494 +++++++++++------- src/features/admin/types.ts | 17 + .../explorer/components/DataExplorer.tsx | 4 +- src/features/workspace/components/HomeTab.tsx | 5 +- .../workspace/components/WorkspaceTabs.tsx | 7 +- src/lib/utils.ts | 4 +- src/pages/Admin.tsx | 146 ++++-- src/pages/Settings.tsx | 7 +- src/store/slices/admin.ts | 147 ++++-- src/store/slices/core.ts | 18 +- src/store/slices/explorer.ts | 6 + src/types/common.ts | 6 +- tailwind.config.js => tailwind.config.ts | 0 15 files changed, 585 insertions(+), 405 deletions(-) delete mode 100644 src/features/admin/components/UpdateUser.tsx rename tailwind.config.js => tailwind.config.ts (100%) diff --git a/components.json b/components.json index 79145dc..26f898d 100644 --- a/components.json +++ b/components.json @@ -2,9 +2,9 @@ "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": false, - "tsx": false, + "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "tailwind.config.ts", "css": "src/index.css", "baseColor": "zinc", "cssVariables": true, diff --git a/src/features/admin/components/UpdateUser.tsx b/src/features/admin/components/UpdateUser.tsx deleted file mode 100644 index f918ab1..0000000 --- a/src/features/admin/components/UpdateUser.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import useAppStore from "@/store"; -import { toast } from 'sonner' - -interface UserData { - name: string; - id: string; - auth_type: string[]; - host_ip: string[]; - default_roles_list: string[]; - default_database: string; -} - -interface UpdateUserProps { - user: UserData; - onClose: () => void; - onUpdated: () => void; -} - -const UpdateUser: React.FC = ({ user, onClose, onUpdated }) => { - const { runQuery } = useAppStore(); - const [authType, setAuthType] = React.useState(user.auth_type.join(", ")); - const [hostIp, setHostIp] = React.useState(user.host_ip.join(", ")); - const [defaultRolesList, setDefaultRolesList] = React.useState(user.default_roles_list.join(", ")); - const [defaultDatabase, setDefaultDatabase] = React.useState(user.default_database); - const [loading, setLoading] = React.useState(false); - - const handleUpdateUser = async () => { - setLoading(true); - try { - const authTypeArray = authType.split(",").map((item) => item.trim()); - const hostIpArray = hostIp.split(",").map((item) => item.trim()); - const rolesArray = defaultRolesList.split(",").map((item) => item.trim()); - - await runQuery(` - ALTER USER ${user.name} - SET AUTH TYPE ${authTypeArray.map((type) => `'${type}'`).join(", ")}, - HOST IP ${hostIpArray.join(", ")}, - DEFAULT ROLE ${rolesArray.join(", ")}, - DEFAULT DATABASE '${defaultDatabase}' - `); - - toast.info(`User ${user.name} updated successfully.`); - onUpdated(); - onClose(); - } catch (error: any) { - toast.error(`Failed to update user: ${error.message}`); - } finally { - setLoading(false); - } - }; - - return ( - - - - Update User - {user.name} - - Update the details for the user "{user.name}" below. - - - -
-
- - setAuthType(e.target.value)} - placeholder="e.g., Password, LDAP" - /> -
-
- - setHostIp(e.target.value)} - placeholder="e.g., 192.168.1.1, 192.168.1.2" - /> -
-
- - setDefaultRolesList(e.target.value)} - placeholder="e.g., admin, editor" - /> -
-
- - setDefaultDatabase(e.target.value)} - placeholder="e.g., my_database" - /> -
-
- - - - - -
-
- ); -}; - -export default UpdateUser; diff --git a/src/features/admin/components/UserTable.tsx b/src/features/admin/components/UserTable.tsx index f0060cf..8dbd718 100644 --- a/src/features/admin/components/UserTable.tsx +++ b/src/features/admin/components/UserTable.tsx @@ -11,6 +11,8 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { @@ -23,6 +25,21 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, +} from "@/components/ui/card"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { MoreHorizontal, KeyRound, @@ -32,45 +49,31 @@ import { Lock, Settings, AlertTriangle, + Search, + RefreshCcw, + Key, + Trash, } from "lucide-react"; import useAppStore from "@/store"; import { toast } from "sonner"; import CreateNewUser from "./CreateNewUser"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { generateRandomPassword } from "@/lib/utils"; +import { UserData } from "../types"; -interface UserData { - name: string; - id: string; - auth_type: string[]; - host_ip: string[]; - host_names: string[]; - host_names_regexp: string[]; - host_names_like: string[]; - default_roles_all: number; - default_roles_list: string[]; - default_database: string; - grantees_any: number; - grantees_list: string[]; - grants?: string[]; - settings_profile?: string; - readonly?: boolean; -} - -interface CreateNewUserProps { - onUserCreated: () => void; -} - -const UserData: React.FC = () => { +const UserTable: React.FC = () => { const { runQuery, isAdmin } = useAppStore(); const [users, setUsers] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(""); const [selectedUser, setSelectedUser] = React.useState(null); const [showDeleteDialog, setShowDeleteDialog] = React.useState(false); + const [showResetPasswordDialog, setShowResetPasswordDialog] = + React.useState(false); const [deleting, setDeleting] = React.useState(false); const [refreshTrigger, setRefreshTrigger] = React.useState(0); + const [searchQuery, setSearchQuery] = React.useState(""); + const [newPassword, setNewPassword] = React.useState(null); const fetchUserGrants = async (username: string) => { try { @@ -101,21 +104,19 @@ const UserData: React.FC = () => { setLoading(true); setError(""); - // Fetch basic user information const usersResult = await runQuery(` - SELECT - name, id, auth_type, host_ip, host_names, - host_names_regexp, host_names_like, default_roles_all, - default_roles_list, default_database, grantees_any, grantees_list - FROM system.users - ORDER BY name ASC - `); + SELECT + name, id, auth_type, host_ip, host_names, + host_names_regexp, host_names_like, default_roles_all, + default_roles_list, default_database, grantees_any, grantees_list + FROM system.users + ORDER BY name ASC + `); if (usersResult.error) { throw new Error(usersResult.error); } - // Enhance user data with grants and settings const enhancedUsers = await Promise.all( usersResult.data.map(async (user: UserData) => { const [grants, settings] = await Promise.all([ @@ -172,9 +173,7 @@ const UserData: React.FC = () => { setDeleting(true); try { - // First revoke all privileges await runQuery(`REVOKE ALL PRIVILEGES ON *.* FROM ${username}`); - // Then drop the user await runQuery(`DROP USER IF EXISTS ${username}`); toast.success(`User ${username} deleted successfully`); setRefreshTrigger((prev) => prev + 1); @@ -188,12 +187,17 @@ const UserData: React.FC = () => { } }; - const refreshUserPassword = async (username: string) => { + const handleRefreshPassword = async (username: string) => { + const password = generateRandomPassword(); + setNewPassword(password); // Store the new password + try { - const password = generateRandomPassword(); - await runQuery(`ALTER USER ${username} SET PASSWORD = '${password}'`); + await runQuery( + `ALTER USER ${username} IDENTIFIED WITH sha256_password BY '${password}'` + ); toast.success(`Password reset for ${username}`); setRefreshTrigger((prev) => prev + 1); + setShowResetPasswordDialog(false); } catch (error: any) { const errorMessage = error.message || "Failed to reset password"; setError(errorMessage); @@ -201,156 +205,292 @@ const UserData: React.FC = () => { } }; - if (loading) { - return ( - <> - - - - - + const filteredUsers = React.useMemo(() => { + return users.filter( + (user) => + user.name.toLowerCase().includes(searchQuery.toLowerCase()) || + user.default_database + ?.toLowerCase() + .includes(searchQuery.toLowerCase()) || + user.default_roles_list?.some((role) => + role.toLowerCase().includes(searchQuery.toLowerCase()) + ) ); + }, [users, searchQuery]); + + const TableSkeletons = () => ( +
+ + + + + +
+ ); + + if (loading) { + return ; } return ( - - {error && ( -
- - {error} -
- )} + + + +
+
+ + User Management + + + Manage database users, roles, and permissions + +
+
+ + {isAdmin && ( + setRefreshTrigger((prev) => prev + 1)} + /> + )} +
+
+
+ + +
+
+ + setSearchQuery(e.target.value)} + className="pl-8" + /> +
+
+ + {error && ( +
+ + {error} +
+ )} -
-

User Management

- {isAdmin && ( - setRefreshTrigger((prev) => prev + 1)} - /> - )} -
+ + + + + User + Authentication + Roles & Database + Access + Status + Actions + + + + {filteredUsers.map((user) => ( + + +
+
+ +
+ {user.name} +
+
+ +
+ {getAuthType(user.auth_type).map((type, idx) => ( + + {type} + + ))} +
+
+ +
+
+ {user.default_roles_all === 1 ? ( + + All Roles + + ) : user.default_roles_list?.length > 0 ? ( + user.default_roles_list.map((role, idx) => ( + + {role} + + )) + ) : ( + + No roles + + )} +
+ + +
+ + {user.default_database || "No default database"} +
+
+ +

Default Database

+
+
+
+
+ + + +
+ + + {getHostAccess(user)} + +
+
+ +

Host Access Configuration

+
+
+
+ +
+ {user.readonly && ( + + + Read-only + + )} + {user.settings_profile && ( + + + {user.settings_profile} + + )} + {user.grantees_any === 1 && ( + + + Can Grant + + )} +
+
+ + + + + + + User Actions + + { + setSelectedUser(user.name); + setShowResetPasswordDialog(true); + }} + > + + Reset Password + + + { + setSelectedUser(user.name); + setShowDeleteDialog(true); + }} + > + + Delete User + + + + +
+ ))} +
+
+
+
+
- - - - User - Authentication - Roles & Database - Access - Status - Actions - - - - {users.map((user) => ( - - -
- - {user.name} -
-
- -
- {getAuthType(user.auth_type).map((type, idx) => ( - - {type} - - ))} -
-
- -
-
- {user.default_roles_all === 1 ? ( - - All Roles - - ) : user.default_roles_list?.length > 0 ? ( - user.default_roles_list.map((role, idx) => ( - - {role} - - )) - ) : ( - No roles - )} -
-
- - {user.default_database || "No default database"} -
-
-
- -
- - {getHostAccess(user)} -
-
- -
- {user.readonly && ( - - - Read-only - - )} - {user.settings_profile && ( - - - {user.settings_profile} - - )} - {user.grantees_any === 1 && ( - - - Can Grant - - )} -
-
- - - - - - - { - setSelectedUser(user.name); - setShowDeleteDialog(true); - }} - className="text-destructive" - > - - Delete User - - - - -
- ))} -
-
+ {/* Reset Password Confirmation Dialog */} + + + + Reset Password + + Are you sure you want to reset the password for "{selectedUser}"? + This action will generate a new password. + + + + + + + + + + {/* New Password Display Dialog */} + setNewPassword(null)}> + + + Password Reset Successful + + The new password for "{selectedUser}" is:{" "} + {newPassword} + + + + + + + + {/* Delete Confirmation Dialog */} Delete User Are you sure you want to delete the user "{selectedUser}"? This - action cannot be undone and will revoke all privileges. + action cannot be undone. @@ -371,8 +511,8 @@ const UserData: React.FC = () => { -
+ ); }; -export default UserData; +export default UserTable; diff --git a/src/features/admin/types.ts b/src/features/admin/types.ts index e69de29..5a2ee88 100644 --- a/src/features/admin/types.ts +++ b/src/features/admin/types.ts @@ -0,0 +1,17 @@ +export interface UserData { + name: string; + id: string; + auth_type: string[]; + host_ip: string[]; + host_names: string[]; + host_names_regexp: string[]; + host_names_like: string[]; + default_roles_all: number; + default_roles_list: string[]; + default_database: string; + grantees_any: number; + grantees_list: string[]; + grants?: string[]; + settings_profile?: string; + readonly?: boolean; + } \ No newline at end of file diff --git a/src/features/explorer/components/DataExplorer.tsx b/src/features/explorer/components/DataExplorer.tsx index 7942433..9758f97 100644 --- a/src/features/explorer/components/DataExplorer.tsx +++ b/src/features/explorer/components/DataExplorer.tsx @@ -19,7 +19,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; - +import { genTabId } from "@/lib/utils"; const DatabaseExplorer: React.FC = () => { const [searchTerm, setSearchTerm] = useState(""); const { @@ -76,7 +76,7 @@ const DatabaseExplorer: React.FC = () => { addTab({ - id: Math.random().toString(36).substr(2, 9), + id: genTabId(), type: "sql", title: "Query", content: "", diff --git a/src/features/workspace/components/HomeTab.tsx b/src/features/workspace/components/HomeTab.tsx index d4ce3d4..f699d4d 100644 --- a/src/features/workspace/components/HomeTab.tsx +++ b/src/features/workspace/components/HomeTab.tsx @@ -20,6 +20,7 @@ import { import useAppStore from "@/store"; import { motion } from "framer-motion"; import { Skeleton } from "@/components/ui/skeleton"; +import { genTabId } from "@/lib/utils"; const quickStartActions = [ { @@ -85,7 +86,7 @@ const HomeTab = () => { const handleNewAction = (type: string, query?: string) => { addTab({ - id: Math.random().toString(36).substr(2, 9), + id: genTabId(), type: "sql", title: query ? `Recent - ${type}` @@ -109,7 +110,7 @@ const HomeTab = () => { FROM system.query_log WHERE - user = 'default' + user = '${credential.username}' AND event_time >= (current_timestamp() - INTERVAL 2 DAY) AND arrayExists(db -> db NOT LIKE '%system%', databases) AND query NOT LIKE 'SELECT DISTINCT%' diff --git a/src/features/workspace/components/WorkspaceTabs.tsx b/src/features/workspace/components/WorkspaceTabs.tsx index 5d122ee..00e9583 100644 --- a/src/features/workspace/components/WorkspaceTabs.tsx +++ b/src/features/workspace/components/WorkspaceTabs.tsx @@ -32,7 +32,7 @@ import useAppStore from "@/store"; import SqlTab from "@/features/workspace/components//SqlTab"; import InformationTab from "@/features/workspace/components//InformationTab"; import { Input } from "@/components/ui/input"; - +import { genTabId } from "@/lib/utils"; interface Tab { id: string; title: string; @@ -153,7 +153,7 @@ export function WorkspaceTabs() { const addNewCodeTab = useCallback(() => { addTab({ - id: Math.random().toString(36).substr(2, 9), + id: genTabId(), title: "Query " + tabs.length, type: "sql", content: "", @@ -177,8 +177,7 @@ export function WorkspaceTabs() { return homeTab ? [homeTab, ...otherTabs] : otherTabs; }, [tabs]); - -/* + /* ***** Keyboard shortcuts COMMENTED OUT ***** useEffect(() => { // Keyboard shortcuts diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 1ee4c91..2c35192 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -38,5 +38,7 @@ export const generateRandomPassword = () => { }; export const genTabId = () => { - return Math.random().toString(36).substr(2, 9); + const timestamp = Date.now().toString(); + const randomStr = Math.random().toString(36).substring(2); + return timestamp + randomStr.slice(0, 26 - timestamp.length); }; diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index 985e152..baf5039 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; - +import { Button } from "@/components/ui/button"; import UserTable from "@/features/admin/components/UserTable"; import { InfoIcon, ShieldCheck } from "lucide-react"; import InfoDialog from "@/components/common/InfoDialog"; @@ -8,52 +8,118 @@ import ClickhouseDefaultConfiguration from "@/features/admin/components/Clickhou export default function Admin() { const [isInfoOpen, setIsInfoOpen] = useState(false); + const [activeSection, setActiveSection] = useState("users"); // Track active section return ( -
-

+
+
+
+

+ + Administration + setIsInfoOpen(true)} + className="text-xs bg-purple-500/40 border border-purple-500 text-purple-600 px-2 py-1 rounded-md cursor-pointer flex items-center gap-1" + > + ALPHA + +

+
+ +
+ {/* Sidebar */} +
+ +
+ + {/* Main Content */} +
+
+ {activeSection === "users" && ( +
+ +
+ )} + + {activeSection === "queries" && ( +
+

+ Saved Queries +

+

+ Manage and activate saved queries. +

+ +
+ )} + + {activeSection === "config" && ( +
+

+ Configuration +

+

+ Manage ClickHouse configuration settings. +

+ +
+ )} +
+
+
+ -
-
    -
  • Here you can manage users, roles, and settings.
  • -
  • - This page is only accessible to administrators. -
  • -
  • - {" "} - All the actions you take here run queries directly on your - ClickHouse system database, be aweare that those can be{" "} - irreversible. -
  • -
-

- This is an ALPHA{" "} - feature and is subject to change. -

-
- - } - variant="info" isOpen={isInfoOpen} onClose={() => setIsInfoOpen(false)} - /> - setIsInfoOpen(true)} - className="text-xs bg-purple-500/40 border border-purple-500 text-purple-600 px-2 py-1 rounded-md cursor-pointer flex items-center gap-1" + variant="info" > - ALPHA - - Administration{" "} -

-
- -
- - -
+
+
    +
  • Here you can manage users, roles, and settings.
  • +
  • + This page is only accessible to administrators. +
  • +
  • + All the actions you take here run queries directly on your + ClickHouse system database, be aware that those can be{" "} + irreversible. +
  • +
+

+ This is an ALPHA feature + and is subject to change. +

+
+
); diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 4ecca5a..b023803 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -291,12 +291,7 @@ export default function SettingsPage() { ) : error ? ( - - - Connection Failed - - {error} - + <> ) : null} diff --git a/src/store/slices/admin.ts b/src/store/slices/admin.ts index 95ca8fd..9cf98e0 100644 --- a/src/store/slices/admin.ts +++ b/src/store/slices/admin.ts @@ -1,8 +1,24 @@ -// src/store/slices/admin.ts import { StateCreator } from 'zustand'; import { AppState, AdminSlice } from '@/types/common'; import { toast } from 'sonner'; +// Define specific error types +export class ClickHouseError extends Error { + constructor(message: string, public readonly originalError?: unknown) { + super(message); + this.name = 'ClickHouseError'; + } +} + +// Define response types +interface AdminCheckResponse { + data: Array<{ is_admin: boolean }>; +} + +interface SavedQueriesCheckResponse { + data: Array<{ exists: number }>; +} + export const createAdminSlice: StateCreator< AppState, [], @@ -18,34 +34,44 @@ export const createAdminSlice: StateCreator< error: null }, - checkIsAdmin: async () => { + checkIsAdmin: async (): Promise => { const { clickHouseClient } = get(); + if (!clickHouseClient) { - throw new Error("ClickHouse client is not initialized"); + throw new ClickHouseError('ClickHouse client is not initialized'); } try { const result = await clickHouseClient.query({ - query: `SELECT if(grant_option = 1, true, false) AS is_admin - FROM system.grants - WHERE user_name = currentUser() - LIMIT 1`, - format: "JSON", + query: ` + SELECT if(grant_option = 1, true, false) AS is_admin + FROM system.grants + WHERE user_name = currentUser() + LIMIT 1 + `, + format: 'JSON', }); - const resultJSON = await result.json(); - if (!Array.isArray(resultJSON.data) || resultJSON.data.length === 0) { - throw new Error("Invalid data format"); + const response = (await result.json()) as AdminCheckResponse; + + if (!Array.isArray(response.data) || response.data.length === 0) { + throw new ClickHouseError('No admin status data returned'); } - set({ isAdmin: resultJSON?.data[0].is_admin as boolean }); + + set({ isAdmin: response.data[0].is_admin }); + return response.data[0].is_admin; + } catch (error) { - console.error("Failed to check admin status:", error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + console.error('Failed to check admin status:', errorMessage); set({ isAdmin: false }); + throw new ClickHouseError('Failed to check admin status', error); } }, - checkSavedQueriesStatus: async () => { + checkSavedQueriesStatus: async (): Promise => { const { runQuery } = get(); + set(state => ({ savedQueries: { ...state.savedQueries, @@ -55,21 +81,35 @@ export const createAdminSlice: StateCreator< })); try { - const result = await runQuery("DESCRIBE CH_UI.saved_queries"); + const result = await runQuery(` + SELECT COUNT(*) as exists + FROM system.tables + WHERE database = 'CH_UI' + AND name = 'saved_queries' + `); + + const response = (result) as SavedQueriesCheckResponse; + const isActive = response.data[0]?.exists > 0; + set(state => ({ savedQueries: { ...state.savedQueries, - isSavedQueriesActive: result.data.length > 0, + isSavedQueriesActive: isActive } })); + + return isActive; + } catch (error) { + const errorMessage = `Failed to check saved queries status: ${error}`; set(state => ({ savedQueries: { ...state.savedQueries, isSavedQueriesActive: false, - error: "Failed to check saved queries status" + error: errorMessage } })); + throw new ClickHouseError(errorMessage, error); } finally { set(state => ({ savedQueries: { @@ -82,6 +122,7 @@ export const createAdminSlice: StateCreator< activateSavedQueries: async () => { const { runQuery } = get(); + set(state => ({ savedQueries: { ...state.savedQueries, @@ -91,29 +132,45 @@ export const createAdminSlice: StateCreator< })); try { - await runQuery("CREATE DATABASE IF NOT EXISTS CH_UI"); - await runQuery(` - CREATE TABLE IF NOT EXISTS CH_UI.saved_queries ( - id String, - name String, - query String, - created_at DateTime, - updated_at DateTime, - owner String, - PRIMARY KEY (id) - ) ENGINE = MergeTree() - `); - - await get().checkSavedQueriesStatus(); - toast.success("Saved queries activated successfully"); + // Run queries in sequence with proper error handling + await runQuery('CREATE DATABASE IF NOT EXISTS CH_UI').then(async () => { + await runQuery(` + CREATE TABLE IF NOT EXISTS CH_UI.saved_queries ( + id String, + name String, + query String, + created_at DateTime64(3), + updated_at DateTime64(3), + owner String, + is_public Boolean DEFAULT false, + tags Array(String) DEFAULT [], + description String DEFAULT '', + PRIMARY KEY (id) + ) ENGINE = MergeTree() + ORDER BY (id, created_at) + SETTINGS index_granularity = 8192 + `); + }); + + // Verify the table was created successfully + const isActive = await get().checkSavedQueriesStatus(); + + if (!isActive) { + throw new ClickHouseError('Table creation verification failed'); + } + + toast.success('Saved queries activated successfully'); + } catch (error) { + const errorMessage = 'Failed to activate saved queries'; set(state => ({ savedQueries: { ...state.savedQueries, - error: "Failed to activate saved queries" + error: errorMessage } })); - toast.error("Failed to activate saved queries"); + toast.error(errorMessage); + throw new ClickHouseError(errorMessage, error); } finally { set(state => ({ savedQueries: { @@ -126,6 +183,7 @@ export const createAdminSlice: StateCreator< deactivateSavedQueries: async () => { const { runQuery } = get(); + set(state => ({ savedQueries: { ...state.savedQueries, @@ -135,17 +193,28 @@ export const createAdminSlice: StateCreator< })); try { - await runQuery("DROP TABLE IF EXISTS CH_UI.saved_queries"); - await get().checkSavedQueriesStatus(); - toast.success("Saved queries deactivated successfully"); + await runQuery('DROP TABLE IF EXISTS CH_UI.saved_queries'); + + // Verify the table was dropped successfully + const isActive = await get().checkSavedQueriesStatus(); + + if (isActive) { + throw new ClickHouseError('Table deletion verification failed'); + } + + toast.success('Saved queries deactivated successfully'); + return true; + } catch (error) { + const errorMessage = 'Failed to deactivate saved queries'; set(state => ({ savedQueries: { ...state.savedQueries, - error: "Failed to deactivate saved queries" + error: errorMessage } })); - toast.error("Failed to deactivate saved queries"); + toast.error(errorMessage); + throw new ClickHouseError(errorMessage, error); } finally { set(state => ({ savedQueries: { diff --git a/src/store/slices/core.ts b/src/store/slices/core.ts index 3756611..91e48a4 100644 --- a/src/store/slices/core.ts +++ b/src/store/slices/core.ts @@ -13,7 +13,11 @@ export const createCoreSlice: StateCreator< [], CoreSlice > = (set, get) => ({ - credential: {} as Credential, + credential: { + host: "", + username: "", + password: "" + }, clickHouseClient: null, isLoadingCredentials: false, isServerAvailable: false, @@ -39,7 +43,9 @@ export const createCoreSlice: StateCreator< clickhouse_settings: get().clickhouseSettings }); set({ clickHouseClient: client }); - await get().checkServerStatus(); + await get().checkServerStatus().then(() => { + get().checkIsAdmin(); + }); } catch (error) { set({ error: (error as Error).message }); toast.error(`Failed to set credentials: ${(error as Error).message}`); @@ -66,12 +72,16 @@ export const createCoreSlice: StateCreator< clearCredentials: async () => { set({ - credential: {} as Credential, + credential: { + host: "", + username: "", + password: "" + }, clickhouseSettings: { max_result_rows: "0", max_result_bytes: "0", result_overflow_mode: "break" - } , + }, clickHouseClient: null, isServerAvailable: false, version: "", diff --git a/src/store/slices/explorer.ts b/src/store/slices/explorer.ts index 9ca5e1f..2113dc1 100644 --- a/src/store/slices/explorer.ts +++ b/src/store/slices/explorer.ts @@ -4,6 +4,12 @@ import { AppState, ExplorerSlice } from '@/types/common'; import { appQueries } from '@/features/workspace/editor/appQueries'; import { toast } from 'sonner'; +interface DatabaseInfo { + name: string; + type: "database"; + children: Array<{ name: string; type: "table" | "view" }>; +} + export const createExplorerSlice: StateCreator< AppState, [], diff --git a/src/types/common.ts b/src/types/common.ts index fce54fb..3e2d9ea 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -101,10 +101,10 @@ export interface AdminState { } export interface AdminSlice extends AdminState { - checkIsAdmin: () => Promise; + checkIsAdmin: () => Promise; activateSavedQueries: () => Promise; - deactivateSavedQueries: () => Promise; - checkSavedQueriesStatus: () => Promise; + deactivateSavedQueries: () => Promise; + checkSavedQueriesStatus: () => Promise; } // src/types/store/index.ts diff --git a/tailwind.config.js b/tailwind.config.ts similarity index 100% rename from tailwind.config.js rename to tailwind.config.ts