client/app/pages/data-sources/schema-table-components/SchemaTable.jsx (255 lines of code) (raw):

import React from "react"; import PropTypes from "prop-types"; import Table from "antd/lib/table"; import Popconfirm from "antd/lib/popconfirm"; import { Schema } from "@/components/proptypes"; import { EditableCell, EditableFormRow, EditableContext } from "./EditableTable"; import TableVisibilityCheckbox from "./TableVisibilityCheckbox"; import "./schema-table.css"; function fetchTableData(schema) { return schema.map(tableData => ({ id: tableData.id, name: tableData.name, description: tableData.description || "", visible: tableData.visible, columns: tableData.columns, sample_queries: tableData.sample_queries || {}, })); } const components = { body: { row: EditableFormRow, cell: EditableCell, }, }; export default class SchemaTable extends React.Component { static propTypes = { schema: Schema, // eslint-disable-line react/no-unused-prop-types updateSchema: PropTypes.func.isRequired, }; static defaultProps = { schema: null, }; constructor(props) { super(props); this.state = { data: [], editingKey: "" }; this.columns = [ { title: "Table Name", dataIndex: "name", width: "18%", key: "name", }, { title: "Table Description", dataIndex: "description", width: "36%", key: "description", editable: true, render: this.truncateDescriptionText, }, { title: "Sample Queries", dataIndex: "sample_queries", width: "24%", key: "sample_queries", editable: true, render: text => ( <ul style={{ margin: 0, paddingLeft: "15px" }}> {Object.values(text).map(query => ( <li key={query.id}> <a target="_blank" rel="noopener noreferrer" href={`queries/${query.id}/source`}> {query.name} </a> </li> ))} </ul> ), }, { title: "Visibility", dataIndex: "visible", width: "10%", key: "visible", editable: true, render: (text, record) => ( <div> <TableVisibilityCheckbox disabled visible={record.visible} /> </div> ), }, { title: "", width: "12%", dataIndex: "edit", key: "edit", // Purposely calling fieldEditor() instead of setting render() to it // because render() will pass a different third argument than what // fieldEditory() takes render: (text, record) => this.fieldEditor(text, record), }, ]; } static getDerivedStateFromProps(nextProps, prevState) { if (nextProps.schema && prevState.data.length === 0) { return { data: fetchTableData(nextProps.schema), editingKey: prevState.editingKey, }; } return prevState; } expandedRowRender = tableData => { const columns = [ { title: "Column Name", dataIndex: "name", key: "name", width: "15%", }, { title: "Column Type", dataIndex: "type", key: "type", width: "15%", }, { title: "Column Example", dataIndex: "example", key: "example", width: "20%", }, { title: "Column Description", dataIndex: "description", key: "description", width: "35%", editable: true, render: this.truncateDescriptionText, onCell: record => ({ record, input_type: "text", dataIndex: "description", title: "Column Description", editing: this.isEditing(record), }), }, { title: "", width: "15%", dataIndex: "edit", key: "edit", render: (text, record) => this.fieldEditor(text, record, tableData), }, ]; return ( <Table components={components} columns={columns} rowKey="id" dataSource={tableData.columns} rowClassName="editable-row" pagination={false} /> ); }; truncateDescriptionText = text => { if (!text) { return; } const MAX_CHARACTER_COUNT = 305; const addEllipses = text.length > MAX_CHARACTER_COUNT; return ( <div title={text}> {`${text.replace(/\n/g, " ").substring(0, MAX_CHARACTER_COUNT)}${addEllipses ? "..." : ""}`} </div> ); }; fieldEditor(text, record, tableData) { const editable = this.isEditing(record); const tableKey = tableData ? tableData.id : record.id; const columnKey = tableData ? record.id : undefined; return ( <div> {editable ? ( <span> <EditableContext.Consumer> {form => ( <a onClick={() => this.save(form, tableKey, columnKey)} style={{ marginRight: 8 }}> Save </a> )} </EditableContext.Consumer> <Popconfirm title="Sure to cancel?" onConfirm={() => this.cancel(record.id)}> <a>Cancel</a> </Popconfirm> </span> ) : ( <a onClick={() => this.edit(record.id)}>Edit</a> )} </div> ); } cancel() { this.setState({ editingKey: "" }); } edit(key) { this.setState({ editingKey: key }); } isEditing(record) { return record.id === this.state.editingKey; } save(form, tableKey, columnKey) { form.validateFields((error, editedFields) => { if (error) { return; } const newData = [...this.state.data]; let spliceIndex = newData.findIndex(item => tableKey === item.id); if (spliceIndex < 0) { return; } const tableRow = newData[spliceIndex]; let dataToUpdate = newData; let rowToUpdate = tableRow; const columnIndex = tableRow.columns.findIndex(item => columnKey === item.id); const columnRow = tableRow.columns[columnIndex]; if (columnKey) { dataToUpdate = tableRow.columns; spliceIndex = columnIndex; rowToUpdate = columnRow; } dataToUpdate.splice(spliceIndex, 1, { ...rowToUpdate, ...editedFields, }); this.props.updateSchema(editedFields, tableRow.id, columnRow ? columnRow.id : undefined); this.setState({ data: newData, editingKey: "" }); }); } render() { const columns = this.columns.map(col => ({ ...col, onCell: record => ({ record, input_type: col.dataIndex, dataIndex: col.dataIndex, title: col.title, editing: col.editable ? this.isEditing(record) : false, }), })); return ( <Table components={components} bordered side="middle" dataSource={this.state.data} pagination={false} columns={columns} rowKey={row => row.id} rowClassName="editable-row" expandedRowRender={this.expandedRowRender} /> ); } }