in desktop/plugins/public/databases/DatabasesPlugin.tsx [351:746]
export function Component() {
const instance = usePlugin(plugin);
const state = useValue(instance.state);
const favorites = useValue(instance.favoritesState);
const onViewModeChanged = useCallback(
(evt: RadioChangeEvent) => {
instance.updateViewMode({viewMode: evt.target.value ?? 'data'});
},
[instance],
);
const onDataClicked = useCallback(() => {
instance.updateViewMode({viewMode: 'data'});
}, [instance]);
const onStructureClicked = useCallback(() => {
instance.updateViewMode({viewMode: 'structure'});
}, [instance]);
const onSQLClicked = useCallback(() => {
instance.updateViewMode({viewMode: 'SQL'});
}, [instance]);
const onTableInfoClicked = useCallback(() => {
instance.updateViewMode({viewMode: 'tableInfo'});
}, [instance]);
const onQueryHistoryClicked = useCallback(() => {
instance.updateViewMode({viewMode: 'queryHistory'});
}, [instance]);
const onRefreshClicked = useCallback(() => {
instance.state.update((state) => {
state.error = null;
});
instance.refresh();
}, [instance]);
const onFavoriteButtonClicked = useCallback(() => {
if (state.query) {
instance.addOrRemoveQueryToFavorites(state.query.value);
}
}, [instance, state.query]);
const onDatabaseSelected = useCallback(
(selected: string) => {
const dbId =
instance.state.get().databases.find((x) => x.name === selected)?.id ||
0;
instance.updateSelectedDatabase({
database: dbId,
});
},
[instance],
);
const onDatabaseTableSelected = useCallback(
(selected: string) => {
instance.updateSelectedDatabaseTable({
table: selected,
});
},
[instance],
);
const onNextPageClicked = useCallback(() => {
instance.nextPage();
}, [instance]);
const onPreviousPageClicked = useCallback(() => {
instance.previousPage();
}, [instance]);
const onExecuteClicked = useCallback(() => {
const query = instance.state.get().query;
if (query) {
instance.execute({query: query.value});
}
}, [instance]);
const onQueryTextareaKeyPress = useCallback(
(event: KeyboardEvent) => {
// Implement ctrl+enter as a shortcut for clicking 'Execute'.
if (event.key === '\n' && event.ctrlKey) {
event.preventDefault();
event.stopPropagation();
onExecuteClicked();
}
},
[onExecuteClicked],
);
const onGoToRow = useCallback(
(row: number, _count: number) => {
instance.goToRow({row: row});
},
[instance],
);
const onQueryChanged = useCallback(
(selected: any) => {
instance.updateQuery({
value: selected.target.value,
});
},
[instance],
);
const onFavoriteQuerySelected = useCallback(
(query: string) => {
instance.updateQuery({
value: query,
});
},
[instance],
);
const pageHighlightedRowsChanged = useCallback(
(rows: TableHighlightedRows) => {
instance.pageHighlightedRowsChanged(rows);
},
[instance],
);
const queryHighlightedRowsChanged = useCallback(
(rows: TableHighlightedRows) => {
instance.queryHighlightedRowsChanged(rows);
},
[instance],
);
const sortOrderChanged = useCallback(
(sortOrder: TableRowSortOrder) => {
instance.sortByChanged({sortOrder});
},
[instance],
);
const onRowEdited = useCallback(
(change: {[key: string]: string | null}) => {
const {selectedDatabaseTable, currentStructure, viewMode, currentPage} =
instance.state.get();
const highlightedRowIdx = currentPage?.highlightedRows[0] ?? -1;
const row =
highlightedRowIdx >= 0
? currentPage?.rows[currentPage?.highlightedRows[0]]
: undefined;
const columns = currentPage?.columns;
// currently only allow to edit data shown in Data tab
if (
viewMode !== 'data' ||
selectedDatabaseTable === null ||
currentStructure === null ||
currentPage === null ||
row === undefined ||
columns === undefined ||
// only trigger when there is change
Object.keys(change).length <= 0
) {
return;
}
// check if the table has primary key to use for query
// This is assumed data are in the same format as in SqliteDatabaseDriver.java
const primaryKeyIdx = currentStructure.columns.indexOf('primary_key');
const nameKeyIdx = currentStructure.columns.indexOf('column_name');
const typeIdx = currentStructure.columns.indexOf('data_type');
const nullableIdx = currentStructure.columns.indexOf('nullable');
if (primaryKeyIdx < 0 && nameKeyIdx < 0 && typeIdx < 0) {
console.error(
'primary_key, column_name, and/or data_type cannot be empty',
);
return;
}
const primaryColumnIndexes = currentStructure.rows
.reduce((acc, row) => {
const primary = row[primaryKeyIdx];
if (primary.type === 'boolean' && primary.value) {
const name = row[nameKeyIdx];
return name.type === 'string' ? acc.concat(name.value) : acc;
} else {
return acc;
}
}, [] as Array<string>)
.map((name) => columns.indexOf(name))
.filter((idx) => idx >= 0);
// stop if no primary key to distinguish unique query
if (primaryColumnIndexes.length <= 0) {
return;
}
const types = currentStructure.rows.reduce((acc, row) => {
const nameValue = row[nameKeyIdx];
const name = nameValue.type === 'string' ? nameValue.value : null;
const typeValue = row[typeIdx];
const type = typeValue.type === 'string' ? typeValue.value : null;
const nullableValue =
nullableIdx < 0 ? {type: 'null', value: null} : row[nullableIdx];
const nullable = nullableValue.value !== false;
if (name !== null && type !== null) {
acc[name] = {type, nullable};
}
return acc;
}, {} as {[key: string]: {type: string; nullable: boolean}});
const changeValue = Object.entries(change).reduce(
(acc, [key, value]: [string, string | null]) => {
acc[key] = convertStringToValue(types, key, value);
return acc;
},
{} as {[key: string]: Value},
);
instance.execute({
query: constructUpdateQuery(
selectedDatabaseTable,
primaryColumnIndexes.reduce((acc, idx) => {
acc[columns[idx]] = row[idx];
return acc;
}, {} as {[key: string]: Value}),
changeValue,
),
});
instance.updatePage({
...produce(currentPage, (draft) =>
Object.entries(changeValue).forEach(
([key, value]: [string, Value]) => {
const columnIdx = draft.columns.indexOf(key);
if (columnIdx >= 0) {
draft.rows[highlightedRowIdx][columnIdx] = value;
}
},
),
),
});
},
[instance],
);
const databaseOptions = useMemoize(
(databases) =>
databases.map((x) => (
<Option key={x.name} value={x.name} label={x.name}>
{x.name}
</Option>
)),
[state.databases],
);
const selectedDatabaseName = useMemoize(
(selectedDatabase: number, databases: DatabaseEntry[]) =>
selectedDatabase && databases[state.selectedDatabase - 1]
? databases[selectedDatabase - 1].name
: undefined,
[state.selectedDatabase, state.databases],
);
const tableOptions = useMemoize(
(selectedDatabase: number, databases: DatabaseEntry[]) =>
selectedDatabase && databases[state.selectedDatabase - 1]
? databases[selectedDatabase - 1].tables.map((tableName) => (
<Option key={tableName} value={tableName} label={tableName}>
{tableName}
</Option>
))
: [],
[state.selectedDatabase, state.databases],
);
const selectedTableName = useMemoize(
(
selectedDatabase: number,
databases: DatabaseEntry[],
selectedDatabaseTable: string | null,
) =>
selectedDatabase && databases[selectedDatabase - 1]
? databases[selectedDatabase - 1].tables.find(
(t) => t === selectedDatabaseTable,
) ?? databases[selectedDatabase - 1].tables[0]
: undefined,
[state.selectedDatabase, state.databases, state.selectedDatabaseTable],
);
return (
<Layout.Container grow>
<Toolbar position="top">
<Radio.Group value={state.viewMode} onChange={onViewModeChanged}>
<Radio.Button value="data" onClick={onDataClicked}>
<TableOutlined style={{marginRight: 5}} />
<Typography.Text>Data</Typography.Text>
</Radio.Button>
<Radio.Button onClick={onStructureClicked} value="structure">
<SettingOutlined style={{marginRight: 5}} />
<Typography.Text>Structure</Typography.Text>
</Radio.Button>
<Radio.Button onClick={onSQLClicked} value="SQL">
<ConsoleSqlOutlined style={{marginRight: 5}} />
<Typography.Text>SQL</Typography.Text>
</Radio.Button>
<Radio.Button onClick={onTableInfoClicked} value="tableInfo">
<DatabaseOutlined style={{marginRight: 5}} />
<Typography.Text>Table Info</Typography.Text>
</Radio.Button>
<Radio.Button onClick={onQueryHistoryClicked} value="queryHistory">
<HistoryOutlined style={{marginRight: 5}} />
<Typography.Text>Query History</Typography.Text>
</Radio.Button>
</Radio.Group>
</Toolbar>
{state.viewMode === 'data' ||
state.viewMode === 'structure' ||
state.viewMode === 'tableInfo' ? (
<Toolbar position="top">
<BoldSpan>Database</BoldSpan>
<Select
showSearch
value={selectedDatabaseName}
onChange={onDatabaseSelected}
style={{flex: 1}}
dropdownMatchSelectWidth={false}>
{databaseOptions}
</Select>
<BoldSpan>Table</BoldSpan>
<Select
showSearch
value={selectedTableName}
onChange={onDatabaseTableSelected}
style={{flex: 1}}
dropdownMatchSelectWidth={false}>
{tableOptions}
</Select>
<div />
<Button onClick={onRefreshClicked} type="default">
Refresh
</Button>
</Toolbar>
) : null}
{state.viewMode === 'SQL' ? (
<Layout.Container>
<Toolbar position="top">
<BoldSpan>Database</BoldSpan>
<Select
showSearch
value={selectedDatabaseName}
onChange={onDatabaseSelected}
dropdownMatchSelectWidth={false}>
{databaseOptions}
</Select>
</Toolbar>
<Layout.Horizontal pad={theme.space.small} style={{paddingBottom: 0}}>
<TextArea
onChange={onQueryChanged}
onKeyPress={onQueryTextareaKeyPress}
placeholder="Type query here.."
value={
state.query !== null && typeof state.query !== 'undefined'
? state.query.value
: undefined
}
/>
</Layout.Horizontal>
<Toolbar position="top">
<Layout.Right>
<div />
<Layout.Horizontal gap={theme.space.small}>
<Button
icon={
state.query && favorites.includes(state.query.value) ? (
<StarFilled />
) : (
<StarOutlined />
)
}
onClick={onFavoriteButtonClicked}
/>
<Dropdown
overlay={
<FavoritesMenu
favorites={favorites}
onClick={onFavoriteQuerySelected}
/>
}>
<Button onClick={() => {}}>
Choose from previous queries <DownOutlined />
</Button>
</Dropdown>
<Button
type="primary"
onClick={onExecuteClicked}
title={'Execute SQL [Ctrl+Return]'}>
Execute
</Button>
</Layout.Horizontal>
</Layout.Right>
</Toolbar>
</Layout.Container>
) : null}