mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
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:
parent
c2a1d30765
commit
f5c7eac7fd
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -8,5 +8,9 @@ interface BrowseRowsContainerProps {
|
||||
}
|
||||
|
||||
export const BrowseRowsContainer = (props: BrowseRowsContainerProps) => {
|
||||
return <DataGrid {...props} />;
|
||||
return (
|
||||
<div className="p-2">
|
||||
<DataGrid {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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 ?? []}
|
||||
|
@ -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);
|
||||
}}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export { EditTableColumnDialog } from './EditTableColumnDialog';
|
||||
export * from './schema';
|
@ -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>;
|
@ -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>}
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
@ -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 };
|
||||
};
|
8
console/src/features/Data/ModifyTable/types.ts
Normal file
8
console/src/features/Data/ModifyTable/types.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { TableColumn } from '@/features/DataSource';
|
||||
|
||||
export type ModifyTableColumn = TableColumn & {
|
||||
config?: {
|
||||
custom_name: string;
|
||||
comment?: string;
|
||||
};
|
||||
};
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user