feature (console): allow user to edit column comments and custom GQL field names for GDC tables

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6469
GitOrigin-RevId: b08f45788966e7bf1636adf55939308432d64397
This commit is contained in:
Vijay Prasanna 2022-10-25 17:45:22 +05:30 committed by hasura-bot
parent c2a1d30765
commit f5c7eac7fd
14 changed files with 316 additions and 31 deletions

View File

@ -75,6 +75,7 @@ const DataSubSidebar = props => {
dataSources,
sidebarLoadingState,
currentTable,
metadataSources,
} = props;
const { setDriver } = useDataSource();
@ -242,7 +243,7 @@ const DataSubSidebar = props => {
);
}, [sources.length, tables, functions, enums, schemaList, currentTable]);
const databasesCount = treeViewItems?.length || 0;
const databasesCount = metadataSources.length;
return (
<div className={`${styles.subSidebarList} ${styles.padd_top_small}`}>
@ -322,6 +323,7 @@ const mapStateToProps = state => {
pathname: state?.routing?.locationBeforeTransitions?.pathname,
dataSources: getDataSources(state),
sidebarLoadingState: state.dataSidebar.loading,
metadataSources: state.metadata.metadataObject.sources,
};
};

View File

@ -8,5 +8,9 @@ interface BrowseRowsContainerProps {
}
export const BrowseRowsContainer = (props: BrowseRowsContainerProps) => {
return <DataGrid {...props} />;
return (
<div className="p-2">
<DataGrid {...props} />
</div>
);
};

View File

@ -88,15 +88,6 @@ export const DataGrid = (props: DataGridProps) => {
if (rows === Feature.NotImplemented)
return <IndicatorCard status="info" headline="Feature Not Implemented" />;
if (isError)
return (
<div className="my-4">
<IndicatorCard status="negative" headline="Something went wrong">
Unable to fetch GraphQL response for table
</IndicatorCard>
</div>
);
return (
<div>
<DataTableOptions
@ -133,6 +124,12 @@ export const DataGrid = (props: DataGridProps) => {
<div className="my-4">
<Skeleton height={30} count={8} className="my-2" />
</div>
) : isError ? (
<div className="my-4">
<IndicatorCard status="negative" headline="Something went wrong">
Unable to fetch GraphQL response for table
</IndicatorCard>
</div>
) : (
<ReactTableWrapper
rows={rows ?? []}

View File

@ -55,6 +55,7 @@ const DisplayWhereClauses = ({
{columnName} {operatorMap[operator]} {value}
</span>
<FaRegTimesCircle
className="cursor-pointer"
onClick={() => {
removeWhereClause(id);
}}
@ -85,6 +86,7 @@ const DisplayOrderByClauses = ({
{orderByClause.column} ({orderByClause.type})
</span>
<FaRegTimesCircle
className="cursor-pointer"
onClick={() => {
removeOrderByClause(id);
}}

View File

@ -12,7 +12,7 @@ export const useTableColumns = ({
}) => {
const httpClient = useHttpClient();
return useQuery({
queryKey: [table, dataSourceName],
queryKey: ['column-introspection', dataSourceName, table],
queryFn: async () => {
const columns = await DataSource(httpClient).getTableColumns({
dataSourceName,

View File

@ -0,0 +1,72 @@
import { Table } from '@/features/MetadataAPI';
import { Button } from '@/new-components/Button';
import { Dialog } from '@/new-components/Dialog';
import { InputField, UpdatedForm } from '@/new-components/Form';
import React from 'react';
import { schema } from './schema';
import { useUpdateTableColumn } from '../../hooks/useUpdateTableColumn';
import { ModifyTableColumn } from '../../types';
interface EditTableColumnDialogProps {
onClose: () => void;
column: ModifyTableColumn;
table: Table;
dataSourceName: string;
}
export const EditTableColumnDialog = (props: EditTableColumnDialogProps) => {
const { onClose, column, table, dataSourceName } = props;
const { updateTableColumn, isLoading: isSaveInProgress } =
useUpdateTableColumn({ table, dataSourceName });
return (
<UpdatedForm
schema={schema}
onSubmit={data => {
updateTableColumn({
column,
updatedConfig: data,
customOnSuccess: onClose,
});
}}
options={{
defaultValues: {
custom_name: column.config?.custom_name ?? '',
comment: column.config?.comment ?? '',
},
}}
>
{() => (
<Dialog
size="md"
description="Edit the columns settings"
title="Modify Column"
hasBackdrop
onClose={onClose}
footer={
<div className="bg-white p-2 justify-end border flex">
<Button type="submit" isLoading={isSaveInProgress}>
Submit
</Button>
</div>
}
>
<div className="m-4">
<InputField
tooltip="Add a comment for your table column"
label="Comment"
name="comment"
placeholder="Add a comment"
/>
<InputField
label="Custom GraphQL field name"
name="custom_name"
placeholder="Enter GraphQL field name"
tooltip="Add a custom GQL field name for table column"
/>
</div>
</Dialog>
)}
</UpdatedForm>
);
};

View File

@ -0,0 +1,2 @@
export { EditTableColumnDialog } from './EditTableColumnDialog';
export * from './schema';

View File

@ -0,0 +1,11 @@
import pickBy from 'lodash.pickby';
import { z } from 'zod';
export const schema = z
.object({
comment: z.string().optional(),
custom_name: z.string().optional(),
})
.transform(value => pickBy(value, d => d !== ''));
export type Schema = z.infer<typeof schema>;

View File

@ -1,32 +1,28 @@
import { TableColumn } from '@/features/DataSource';
import { Badge } from '@/new-components/Badge';
import { Button } from '@/new-components/Button';
import { useFireNotification } from '@/new-components/Notifications';
import React from 'react';
import { FaKey } from 'react-icons/fa';
import { ModifyTableColumn } from '../types';
export const TableColumnDescription: React.VFC<{
column: TableColumn;
}> = ({ column }) => {
const { fireNotification } = useFireNotification();
column: ModifyTableColumn;
onEdit: (column: ModifyTableColumn) => void;
}> = ({ column, onEdit }) => {
return (
<div key={column.name} className="flex gap-4 items-center mb-2">
<Button
size="sm"
// disabled
onClick={() => {
fireNotification({
message: 'This feature is coming soon.',
title: 'Feature Not Available',
type: 'info',
});
onEdit(column);
}}
>
Edit
</Button>
<strong>{column.name}</strong>
<div>
<div className="font-bold">{column.name}</div>
<div className="italic">{column.config?.comment}</div>
</div>
<div>{column.dataType}</div>
{column.nullable && <Badge color="gray">nullable</Badge>}

View File

@ -1,15 +1,51 @@
import { useTableColumns } from '@/features/BrowseRows';
import React from 'react';
import { IndicatorCard } from '@/new-components/IndicatorCard';
import React, { useState } from 'react';
import Skeleton from 'react-loading-skeleton';
import { ManageTableProps } from '../../ManageTable';
import { useListAllTableColumns } from '../hooks/useListAllTableColumns';
import { ModifyTableColumn } from '../types';
import { EditTableColumnDialog } from './EditTableColumnDialog/EditTableColumnDialog';
import { TableColumnDescription } from './TableColumnDescription';
export const TableColumns: React.VFC<ManageTableProps> = props => {
const columns = useTableColumns(props);
const { data: columns, isLoading, isError } = useListAllTableColumns(props);
const [isEditColumnFormActive, setIsEditColumnFormActive] = useState(false);
const [selectedColumn, setSelectedColumn] = useState<ModifyTableColumn>();
const resetDialogState = () => {
setSelectedColumn(undefined);
setIsEditColumnFormActive(false);
};
if (isLoading || !columns) return <Skeleton count={5} height={20} />;
if (isError)
return (
<IndicatorCard status="negative" headline="error">
Unable to fetch columns
</IndicatorCard>
);
return (
<>
{columns.map(c => (
<TableColumnDescription column={c} key={c.name} />
{(columns ?? []).map(c => (
<TableColumnDescription
column={c}
key={c.name}
onEdit={column => {
setIsEditColumnFormActive(true);
setSelectedColumn(column);
}}
/>
))}
{isEditColumnFormActive && selectedColumn && (
<EditTableColumnDialog
{...props}
column={selectedColumn}
onClose={resetDialogState}
/>
)}
</>
);
};

View File

@ -0,0 +1,43 @@
// eslint-disable-next-line no-restricted-imports
import { useTableColumns } from '@/features/BrowseRows/components/DataGrid/useTableColumns';
import { exportMetadata } from '@/features/DataSource';
import { Table } from '@/features/MetadataAPI';
import { useHttpClient } from '@/features/Network';
import { areTablesEqual } from '@/features/RelationshipsTable';
import { useQuery } from 'react-query';
export const useListAllTableColumns = ({
table,
dataSourceName,
}: {
table: Table;
dataSourceName: string;
}) => {
const httpClient = useHttpClient();
const { data: tableColumns, isSuccess: isIntrospectionReady } =
useTableColumns({
table,
dataSourceName,
});
return useQuery({
queryKey: ['modify', 'columns', dataSourceName, table],
queryFn: async () => {
const { metadata } = await exportMetadata({ httpClient });
const metadataTable = metadata?.sources
.find(s => s.name === dataSourceName)
?.tables.find(t => areTablesEqual(t.table, table));
const tableConfig = metadataTable?.configuration?.column_config;
return (tableColumns?.columns ?? []).map(tableColumn => {
return {
...tableColumn,
config: tableConfig?.[tableColumn.name],
};
});
},
enabled: isIntrospectionReady,
});
};

View File

@ -0,0 +1,112 @@
import { exportMetadata, TableColumn } from '@/features/DataSource';
import { Table, useMetadataMigration } from '@/features/MetadataAPI';
import { useHttpClient } from '@/features/Network';
import { areTablesEqual } from '@/features/RelationshipsTable';
import { useFireNotification } from '@/new-components/Notifications';
import { useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { Schema } from '../components/EditTableColumnDialog';
export const useUpdateTableColumn = ({
dataSourceName,
table,
}: {
dataSourceName: string;
table: Table;
}) => {
const httpClient = useHttpClient();
const { mutate, ...rest } = useMetadataMigration();
const { fireNotification } = useFireNotification();
const queryClient = useQueryClient();
const updateTableColumn = useCallback(
async ({
column,
updatedConfig,
customOnSuccess,
customOnError,
}: {
column: TableColumn;
updatedConfig: Schema;
customOnSuccess?: () => void;
customOnError?: (err: unknown) => void;
}) => {
const { metadata, resource_version } = await exportMetadata({
httpClient,
});
if (!metadata) throw Error('Cannot find metadata info');
const metadataSource = metadata.sources.find(
s => s.name === dataSourceName
);
if (!metadataSource) throw Error('Cannot find source in metadata');
const metadataTable = metadataSource.tables.find(t =>
areTablesEqual(t.table, table)
);
if (!metadataTable) throw Error('Cannot find table');
const driver = metadataSource.kind;
const type = 'set_table_customization';
mutate(
{
query: {
resource_version,
type: `${driver}_${type}`,
args: {
table,
source: dataSourceName,
configuration: {
...metadataTable.configuration,
column_config: {
...metadataTable.configuration?.column_config,
[column.name]: updatedConfig,
},
},
},
},
},
{
onSuccess: () => {
fireNotification({
title: 'Success!',
message: 'Successfully updated column!',
type: 'success',
});
queryClient.refetchQueries([
'modify',
'columns',
dataSourceName,
table,
]);
if (customOnSuccess) {
customOnSuccess();
}
},
onError: err => {
fireNotification({
title: 'Error!',
message: JSON.stringify(err),
type: 'error',
});
if (customOnError) {
customOnError(err);
}
},
}
);
},
[httpClient, mutate, table, dataSourceName, fireNotification, queryClient]
);
return { updateTableColumn, ...rest };
};

View File

@ -0,0 +1,8 @@
import { TableColumn } from '@/features/DataSource';
export type ModifyTableColumn = TableColumn & {
config?: {
custom_name: string;
comment?: string;
};
};

View File

@ -60,7 +60,7 @@ export type DialogProps = {
children: string | React.ReactElement;
title: string;
description?: string;
hasBackdrop: boolean;
hasBackdrop?: boolean;
onClose?: () => void;
footer?: FooterProps | React.ReactElement;
size?: DialogSize;