Skip to content

Commit

Permalink
Merge pull request #112 from jdeniau/navigate-through-database
Browse files Browse the repository at this point in the history
Navigate through database with NavigateModal
  • Loading branch information
jdeniau authored Oct 8, 2024
2 parents 6b67f99 + 74505c4 commit ad436d3
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 57 deletions.
32 changes: 32 additions & 0 deletions src/contexts/DatabaseListContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createContext, useContext } from 'react';
import { ShowDatabasesResult } from '../sql/types';

export type DatabaseListContext = ShowDatabasesResult;

const DatabaseListContext = createContext<DatabaseListContext | null>(null);

export function DatabaseListContextProvider({
children,
databaseList: DatabaseList,
}: {
children: React.ReactNode;
databaseList: DatabaseListContext;
}) {
return (
<DatabaseListContext.Provider value={DatabaseList}>
{children}
</DatabaseListContext.Provider>
);
}

export function useDatabaseListContext(): DatabaseListContext {
const context = useContext(DatabaseListContext);

if (context === null) {
throw new Error(
'useTableListContext must be used inside a TableListContextProvider'
);
}

return context;
}
38 changes: 24 additions & 14 deletions src/renderer/component/NavigateModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { action } from '@storybook/addon-actions';
import type { Meta, StoryObj } from '@storybook/react';
import reactRouterDecorator from '../../../.storybook/decorators/reactRouterDecorator';
import { DatabaseListContextProvider } from '../../contexts/DatabaseListContext';
import { TableListContextProvider } from '../../contexts/TableListContext';
import NavigateModal from './NavigateModal';
// eslint-disable-next-line import/no-unresolved
Expand All @@ -25,24 +26,33 @@ type Story = StoryObj<typeof NavigateModal>;
export const Primary: Story = {
decorators: [
(Story) => (
<TableListContextProvider
tableList={[
<DatabaseListContextProvider
databaseList={[
// @ts-expect-error don't want all data, only the name
{ Name: 'departments' },
{ Database: 'mysql' },
// @ts-expect-error don't want all data, only the name
{ Name: 'dept_emp' },
// @ts-expect-error don't want all data, only the name
{ Name: 'dept_manager' },
// @ts-expect-error don't want all data, only the name
{ Name: 'employees' },
// @ts-expect-error don't want all data, only the name
{ Name: 'salaries' },
// @ts-expect-error don't want all data, only the name
{ Name: 'titles' },
{ Database: 'users' },
]}
>
<Story />
</TableListContextProvider>
<TableListContextProvider
tableList={[
// @ts-expect-error don't want all data, only the name
{ Name: 'departments' },
// @ts-expect-error don't want all data, only the name
{ Name: 'dept_emp' },
// @ts-expect-error don't want all data, only the name
{ Name: 'dept_manager' },
// @ts-expect-error don't want all data, only the name
{ Name: 'employees' },
// @ts-expect-error don't want all data, only the name
{ Name: 'salaries' },
// @ts-expect-error don't want all data, only the name
{ Name: 'titles' },
]}
>
<Story />
</TableListContextProvider>
</DatabaseListContextProvider>
),
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,51 @@ import { Flex, Input, InputRef, List, Modal } from 'antd';
import Fuse from 'fuse.js';
import { useNavigate } from 'react-router-dom';
import { styled } from 'styled-components';
import { useConnectionContext } from '../../contexts/ConnectionContext';
import { useDatabaseContext } from '../../contexts/DatabaseContext';
import { useTableListContext } from '../../contexts/TableListContext';
import { useTranslation } from '../../i18n';
import { ShowTableStatus } from '../../sql/types';
import { selection } from '../theme';
import { useTranslation } from '../../../i18n';
import { selection } from '../../theme';

export type NavigationItem = {
key: string;
name: string;
link: string;
Icon: ReactElement;
};

type Props = {
isNavigateModalOpen: boolean;
setIsNavigateModalOpen: (isOpened: boolean) => void;
navigationItemList: Array<NavigationItem>;
};

export default function NavigateModal({
isNavigateModalOpen,
setIsNavigateModalOpen,
navigationItemList,
}: Props): ReactElement {
const { t } = useTranslation();
const [searchText, setSearchText] = useState('');
const { currentConnectionSlug } = useConnectionContext();
const { database } = useDatabaseContext();
const navigate = useNavigate();
const searchRef = useRef<InputRef>(null);
const [activeIndex, setActiveIndex] = useState(-1);
const tableStatusList = useTableListContext();

let filteredTableStatusList = tableStatusList;
let filteredTableStatusList = navigationItemList;

if (searchText) {
const fuze = new Fuse(tableStatusList ?? [], {
keys: ['Name'],
const fuze = new Fuse(navigationItemList, {
keys: ['name'],
threshold: 0.4,
});

filteredTableStatusList = fuze.search(searchText).map((item) => item.item);
}

const navigateToItem = useCallback(
(item: ShowTableStatus) => {
(item: NavigationItem) => {
setIsNavigateModalOpen(false);
navigate(
`/connections/${currentConnectionSlug}/${database}/tables/${item.Name}`
);

navigate(item.link);
},
[currentConnectionSlug, database, navigate, setIsNavigateModalOpen]
[navigate, setIsNavigateModalOpen]
);

// focus input when the modal shows up
Expand Down Expand Up @@ -138,15 +139,15 @@ export default function NavigateModal({
style={{ overflow: 'auto' }}
bordered
dataSource={filteredTableStatusList}
renderItem={(item: ShowTableStatus, index: number) => (
renderItem={(item: NavigationItem, index: number) => (
<ItemListWithHover
key={item.Name}
key={item.key}
$active={index === activeIndex}
onClick={() => {
navigateToItem(item);
}}
>
{item.Name}
{item.Icon} {item.name}
</ItemListWithHover>
)}
/>
Expand All @@ -155,7 +156,9 @@ export default function NavigateModal({
);
}

const ItemListWithHover = styled(List.Item)<{ $active: boolean }>`
const ItemListWithHover = styled(List.Item)<{
$active: boolean;
}>`
cursor: pointer;
&:hover {
Expand Down
39 changes: 39 additions & 0 deletions src/renderer/component/NavigateModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useMemo } from 'react';
import { DatabaseOutlined, TableOutlined } from '@ant-design/icons';
import { useConnectionContext } from '../../../contexts/ConnectionContext';
import { useDatabaseContext } from '../../../contexts/DatabaseContext';
import { useDatabaseListContext } from '../../../contexts/DatabaseListContext';
import { useTableListContext } from '../../../contexts/TableListContext';
import NavigateModal, { NavigationItem } from './NavigateModal';

type Props = {
isNavigateModalOpen: boolean;
setIsNavigateModalOpen: (isOpened: boolean) => void;
};

export default function NavigateModalContainer(props: Props): JSX.Element {
const tableStatusList = useTableListContext();
const { currentConnectionSlug } = useConnectionContext();
const { database } = useDatabaseContext();
const databaseList = useDatabaseListContext();

const navigationItemList: Array<NavigationItem> = useMemo(
() => [
...tableStatusList.map((table) => ({
key: `Table-${table.Name}`,
name: table.Name,
link: `/connections/${currentConnectionSlug}/${database}/tables/${table.Name}`,
Icon: <TableOutlined />,
})),
...databaseList.map((showDatabase) => ({
key: `Database-${showDatabase.Database}`,
name: showDatabase.Database,
link: `/connections/${currentConnectionSlug}/${showDatabase.Database}`,
Icon: <DatabaseOutlined />,
})),
],
[currentConnectionSlug, database, databaseList, tableStatusList]
);

return <NavigateModal navigationItemList={navigationItemList} {...props} />;
}
43 changes: 23 additions & 20 deletions src/renderer/routes/connections.$connectionSlug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { styled } from 'styled-components';
import invariant from 'tiny-invariant';
import { AllColumnsContextProvider } from '../../contexts/AllColumnsContext';
import { useConnectionContext } from '../../contexts/ConnectionContext';
import { DatabaseListContextProvider } from '../../contexts/DatabaseListContext';
import { ForeignKeysContextProvider } from '../../contexts/ForeignKeysContext';
import { TableListContextProvider } from '../../contexts/TableListContext';
import { useTranslation } from '../../i18n';
Expand Down Expand Up @@ -107,26 +108,28 @@ export default function ConnectionDetailPage() {
}, [addConnectionToList, connectionSlug]);

return (
<TableListContextProvider tableList={tableStatusList}>
<ForeignKeysContextProvider keyColumnUsageRows={keyColumnUsageRows}>
<AllColumnsContextProvider allColumns={allColumns}>
<NavigateModalContextProvider>
<Layout>
<Sider width={200} style={{ overflow: 'auto' }}>
<Flex vertical gap="small">
<DatabaseSelector databaseList={databaseList} />
<OpenNavigateModalButton />
<TableList tableStatusList={tableStatusList} />
</Flex>
</Sider>
<Layout.Content style={{ overflow: 'auto' }}>
<Outlet />
</Layout.Content>
</Layout>
</NavigateModalContextProvider>
</AllColumnsContextProvider>
</ForeignKeysContextProvider>
</TableListContextProvider>
<DatabaseListContextProvider databaseList={databaseList}>
<TableListContextProvider tableList={tableStatusList}>
<ForeignKeysContextProvider keyColumnUsageRows={keyColumnUsageRows}>
<AllColumnsContextProvider allColumns={allColumns}>
<NavigateModalContextProvider>
<Layout>
<Sider width={200} style={{ overflow: 'auto' }}>
<Flex vertical gap="small">
<DatabaseSelector databaseList={databaseList} />
<OpenNavigateModalButton />
<TableList tableStatusList={tableStatusList} />
</Flex>
</Sider>
<Layout.Content style={{ overflow: 'auto' }}>
<Outlet />
</Layout.Content>
</Layout>
</NavigateModalContextProvider>
</AllColumnsContextProvider>
</ForeignKeysContextProvider>
</TableListContextProvider>
</DatabaseListContextProvider>
);
}

Expand Down
6 changes: 5 additions & 1 deletion src/sql/type-guard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ResultSetHeader, RowDataPacket } from 'mysql2';
import { QueryReturnType } from './types';
import { QueryReturnType, ShowDatabaseRow } from './types';

function isObjectAResultSetHeader(obj: object): obj is ResultSetHeader {
// test all keys that are present in ResultSetHeader (except deprecated "changedRows")
Expand Down Expand Up @@ -70,3 +70,7 @@ export function isRowDataPacketArray(
}

// TODO handle ProcedureCallPacket ?

export function isShowDatabaseRow(obj: object): obj is ShowDatabaseRow {
return 'Database' in obj;
}
2 changes: 1 addition & 1 deletion src/sql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type ConnectionObjectWithoutSlug = Omit<ConnectionObject, 'slug'>;
/**
* Represent the return type of "SHOW DATABASES;" query.
*/
interface ShowDatabaseRow extends RowDataPacket {
export interface ShowDatabaseRow extends RowDataPacket {
Database: string;
}
export type ShowDatabasesResult = ShowDatabaseRow[];
Expand Down

0 comments on commit ad436d3

Please sign in to comment.