mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-16 01:44:03 +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,
|
dataSources,
|
||||||
sidebarLoadingState,
|
sidebarLoadingState,
|
||||||
currentTable,
|
currentTable,
|
||||||
|
metadataSources,
|
||||||
} = props;
|
} = props;
|
||||||
const { setDriver } = useDataSource();
|
const { setDriver } = useDataSource();
|
||||||
|
|
||||||
@ -242,7 +243,7 @@ const DataSubSidebar = props => {
|
|||||||
);
|
);
|
||||||
}, [sources.length, tables, functions, enums, schemaList, currentTable]);
|
}, [sources.length, tables, functions, enums, schemaList, currentTable]);
|
||||||
|
|
||||||
const databasesCount = treeViewItems?.length || 0;
|
const databasesCount = metadataSources.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.subSidebarList} ${styles.padd_top_small}`}>
|
<div className={`${styles.subSidebarList} ${styles.padd_top_small}`}>
|
||||||
@ -322,6 +323,7 @@ const mapStateToProps = state => {
|
|||||||
pathname: state?.routing?.locationBeforeTransitions?.pathname,
|
pathname: state?.routing?.locationBeforeTransitions?.pathname,
|
||||||
dataSources: getDataSources(state),
|
dataSources: getDataSources(state),
|
||||||
sidebarLoadingState: state.dataSidebar.loading,
|
sidebarLoadingState: state.dataSidebar.loading,
|
||||||
|
metadataSources: state.metadata.metadataObject.sources,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,5 +8,9 @@ interface BrowseRowsContainerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BrowseRowsContainer = (props: 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)
|
if (rows === Feature.NotImplemented)
|
||||||
return <IndicatorCard status="info" headline="Feature Not Implemented" />;
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<DataTableOptions
|
<DataTableOptions
|
||||||
@ -133,6 +124,12 @@ export const DataGrid = (props: DataGridProps) => {
|
|||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
<Skeleton height={30} count={8} className="my-2" />
|
<Skeleton height={30} count={8} className="my-2" />
|
||||||
</div>
|
</div>
|
||||||
|
) : isError ? (
|
||||||
|
<div className="my-4">
|
||||||
|
<IndicatorCard status="negative" headline="Something went wrong">
|
||||||
|
Unable to fetch GraphQL response for table
|
||||||
|
</IndicatorCard>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ReactTableWrapper
|
<ReactTableWrapper
|
||||||
rows={rows ?? []}
|
rows={rows ?? []}
|
||||||
|
@ -55,6 +55,7 @@ const DisplayWhereClauses = ({
|
|||||||
{columnName} {operatorMap[operator]} {value}
|
{columnName} {operatorMap[operator]} {value}
|
||||||
</span>
|
</span>
|
||||||
<FaRegTimesCircle
|
<FaRegTimesCircle
|
||||||
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
removeWhereClause(id);
|
removeWhereClause(id);
|
||||||
}}
|
}}
|
||||||
@ -85,6 +86,7 @@ const DisplayOrderByClauses = ({
|
|||||||
{orderByClause.column} ({orderByClause.type})
|
{orderByClause.column} ({orderByClause.type})
|
||||||
</span>
|
</span>
|
||||||
<FaRegTimesCircle
|
<FaRegTimesCircle
|
||||||
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
removeOrderByClause(id);
|
removeOrderByClause(id);
|
||||||
}}
|
}}
|
||||||
|
@ -12,7 +12,7 @@ export const useTableColumns = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const httpClient = useHttpClient();
|
const httpClient = useHttpClient();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [table, dataSourceName],
|
queryKey: ['column-introspection', dataSourceName, table],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const columns = await DataSource(httpClient).getTableColumns({
|
const columns = await DataSource(httpClient).getTableColumns({
|
||||||
dataSourceName,
|
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 { Badge } from '@/new-components/Badge';
|
||||||
import { Button } from '@/new-components/Button';
|
import { Button } from '@/new-components/Button';
|
||||||
import { useFireNotification } from '@/new-components/Notifications';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FaKey } from 'react-icons/fa';
|
import { FaKey } from 'react-icons/fa';
|
||||||
|
import { ModifyTableColumn } from '../types';
|
||||||
|
|
||||||
export const TableColumnDescription: React.VFC<{
|
export const TableColumnDescription: React.VFC<{
|
||||||
column: TableColumn;
|
column: ModifyTableColumn;
|
||||||
}> = ({ column }) => {
|
onEdit: (column: ModifyTableColumn) => void;
|
||||||
const { fireNotification } = useFireNotification();
|
}> = ({ column, onEdit }) => {
|
||||||
return (
|
return (
|
||||||
<div key={column.name} className="flex gap-4 items-center mb-2">
|
<div key={column.name} className="flex gap-4 items-center mb-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
// disabled
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
fireNotification({
|
onEdit(column);
|
||||||
message: 'This feature is coming soon.',
|
|
||||||
title: 'Feature Not Available',
|
|
||||||
type: 'info',
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</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>
|
<div>{column.dataType}</div>
|
||||||
|
|
||||||
{column.nullable && <Badge color="gray">nullable</Badge>}
|
{column.nullable && <Badge color="gray">nullable</Badge>}
|
||||||
|
@ -1,15 +1,51 @@
|
|||||||
import { useTableColumns } from '@/features/BrowseRows';
|
import { IndicatorCard } from '@/new-components/IndicatorCard';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
import { ManageTableProps } from '../../ManageTable';
|
import { ManageTableProps } from '../../ManageTable';
|
||||||
|
import { useListAllTableColumns } from '../hooks/useListAllTableColumns';
|
||||||
|
import { ModifyTableColumn } from '../types';
|
||||||
|
import { EditTableColumnDialog } from './EditTableColumnDialog/EditTableColumnDialog';
|
||||||
import { TableColumnDescription } from './TableColumnDescription';
|
import { TableColumnDescription } from './TableColumnDescription';
|
||||||
|
|
||||||
export const TableColumns: React.VFC<ManageTableProps> = props => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{columns.map(c => (
|
{(columns ?? []).map(c => (
|
||||||
<TableColumnDescription column={c} key={c.name} />
|
<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;
|
children: string | React.ReactElement;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
hasBackdrop: boolean;
|
hasBackdrop?: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
footer?: FooterProps | React.ReactElement;
|
footer?: FooterProps | React.ReactElement;
|
||||||
size?: DialogSize;
|
size?: DialogSize;
|
||||||
|
Loading…
Reference in New Issue
Block a user