mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-31 12:39:01 +00:00
UI : Moving Sample data to its separate tab. (#2045)
* UI : Moving Sample data to its separate tab. * Adding horizontal scrolling indicator for sample table
This commit is contained in:
parent
dea0f2117d
commit
83c452431e
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="512" height="512" x="0" y="0" viewBox="0 0 32 32" style="enable-background:new 0 0 512 512" xml:space="preserve" class=""><g><g xmlns="http://www.w3.org/2000/svg" id="Layer_2" data-name="Layer 2"><path d="m26.4 31h-16.63a2.69 2.69 0 0 1 -2.69-2.69v-24.62a2.69 2.69 0 0 1 2.69-2.69h7.63a2.78 2.78 0 0 1 1.85.71l8.9 8a2.79 2.79 0 0 1 .93 2.08v16.52a2.69 2.69 0 0 1 -2.68 2.69zm-16.63-28a.69.69 0 0 0 -.69.69v24.62a.69.69 0 0 0 .69.69h16.63a.69.69 0 0 0 .69-.69v-16.57a.78.78 0 0 0 -.26-.58l-8.9-8a.78.78 0 0 0 -.53-.16z" fill="#7147E8" data-original="#000000" class=""></path><path d="m28.08 11.94h-8.81a2.19 2.19 0 0 1 -2.19-2.19v-7.19h2v7.19a.19.19 0 0 0 .19.19h8.81z" fill="#7147E8" data-original="#000000" class=""></path><rect height="2" rx="1" width="10" x="13.42" y="24" fill="#7147E8" data-original="#000000" class=""></rect><rect height="2" rx="1" width="4" x="13.42" y="19.69" fill="#7147E8" data-original="#000000" class=""></rect><path d="m5.41 26.66a2.52 2.52 0 0 1 -2.49-2.51v-16.3a2.5 2.5 0 0 1 3-2.45l2.15.36-.33 2-2.17-.39a.53.53 0 0 0 -.46.09.5.5 0 0 0 -.19.39v16.3a.51.51 0 0 0 .61.49l2.26-.3.26 2-2.19.29a2.11 2.11 0 0 1 -.45.03z" fill="#7147E8" data-original="#000000" class=""></path></g></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="512" height="512" x="0" y="0" viewBox="0 0 32 32" style="enable-background:new 0 0 512 512" xml:space="preserve" class=""><g><g xmlns="http://www.w3.org/2000/svg" id="Layer_2" data-name="Layer 2"><path d="m26.4 31h-16.63a2.69 2.69 0 0 1 -2.69-2.69v-24.62a2.69 2.69 0 0 1 2.69-2.69h7.63a2.78 2.78 0 0 1 1.85.71l8.9 8a2.79 2.79 0 0 1 .93 2.08v16.52a2.69 2.69 0 0 1 -2.68 2.69zm-16.63-28a.69.69 0 0 0 -.69.69v24.62a.69.69 0 0 0 .69.69h16.63a.69.69 0 0 0 .69-.69v-16.57a.78.78 0 0 0 -.26-.58l-8.9-8a.78.78 0 0 0 -.53-.16z" fill="#6b7280" data-original="#000000" class=""></path><path d="m28.08 11.94h-8.81a2.19 2.19 0 0 1 -2.19-2.19v-7.19h2v7.19a.19.19 0 0 0 .19.19h8.81z" fill="#6b7280" data-original="#000000" class=""></path><rect height="2" rx="1" width="10" x="13.42" y="24" fill="#6b7280" data-original="#000000" class=""></rect><rect height="2" rx="1" width="4" x="13.42" y="19.69" fill="#6b7280" data-original="#000000" class=""></rect><path d="m5.41 26.66a2.52 2.52 0 0 1 -2.49-2.51v-16.3a2.5 2.5 0 0 1 3-2.45l2.15.36-.33 2-2.17-.39a.53.53 0 0 0 -.46.09.5.5 0 0 0 -.19.39v16.3a.51.51 0 0 0 .61.49l2.26-.3.26 2-2.19.29a2.11 2.11 0 0 1 -.45.03z" fill="#6b7280" data-original="#000000" class=""></path></g></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -40,6 +40,9 @@ import PageContainer from '../containers/PageContainer';
|
||||
import Entitylineage from '../EntityLineage/EntityLineage.component';
|
||||
import FrequentlyJoinedTables from '../FrequentlyJoinedTables/FrequentlyJoinedTables.component';
|
||||
import ManageTab from '../ManageTab/ManageTab.component';
|
||||
import SampleDataTable, {
|
||||
SampleColumns,
|
||||
} from '../SampleDataTable/SampleDataTable.component';
|
||||
import SchemaEditor from '../schema-editor/SchemaEditor';
|
||||
import SchemaTab from '../SchemaTab/SchemaTab.component';
|
||||
import TableProfiler from '../TableProfiler/TableProfiler.component';
|
||||
@ -164,6 +167,17 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
isHidden: !dataModel?.sql,
|
||||
position: 4,
|
||||
},
|
||||
{
|
||||
name: 'Sample Data',
|
||||
icon: {
|
||||
alt: 'sample_data',
|
||||
name: 'sample-data',
|
||||
title: 'Sample Data',
|
||||
selectedName: 'sample-data-color',
|
||||
},
|
||||
isProtected: false,
|
||||
position: 5,
|
||||
},
|
||||
{
|
||||
name: 'Manage',
|
||||
icon: {
|
||||
@ -174,7 +188,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
},
|
||||
isProtected: true,
|
||||
protectedState: !owner || hasEditAccess(),
|
||||
position: 5,
|
||||
position: 6,
|
||||
},
|
||||
];
|
||||
|
||||
@ -331,6 +345,29 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleDataWithType = () => {
|
||||
const updatedColumns = sampleData?.columns?.map((column) => {
|
||||
const matchedColumn = columns.find((col) => col.name === column);
|
||||
|
||||
if (matchedColumn) {
|
||||
return {
|
||||
name: matchedColumn.name,
|
||||
dataType: matchedColumn.dataType,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: column,
|
||||
dataType: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
columns: updatedColumns as SampleColumns[] | undefined,
|
||||
rows: sampleData?.rows,
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthDisabled && users.length && followers.length) {
|
||||
setFollowersData(followers);
|
||||
@ -450,6 +487,11 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 5 && (
|
||||
<div className="tw-mt-4">
|
||||
<SampleDataTable sampleData={getSampleDataWithType()} />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 6 && (
|
||||
<div className="tw-mt-4">
|
||||
<ManageTab
|
||||
currentTier={tier?.tagFQN}
|
||||
|
@ -13,7 +13,13 @@
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { lowerCase } from 'lodash';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { TableData } from '../../generated/entity/data/table';
|
||||
import { isEven } from '../../utils/CommonUtils';
|
||||
|
||||
@ -27,59 +33,108 @@ type Props = {
|
||||
};
|
||||
|
||||
const SampleDataTable: FunctionComponent<Props> = ({ sampleData }: Props) => {
|
||||
const tableRef = useRef<HTMLDivElement>(null);
|
||||
const [scrollOffset, setScrollOffSet] = useState<number>(0);
|
||||
const [containerWidth, setContainerWidth] = useState<number>(0);
|
||||
const [scrollHandle, setScrollHandle] = useState<{
|
||||
left: boolean;
|
||||
right: boolean;
|
||||
}>({ left: true, right: true });
|
||||
|
||||
const scrollHandler = (scrollOffset: number) => {
|
||||
if (tableRef.current) {
|
||||
tableRef.current.scrollLeft += scrollOffset;
|
||||
setScrollOffSet(tableRef.current.scrollLeft);
|
||||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setContainerWidth(
|
||||
(tableRef.current?.scrollWidth ?? 0) -
|
||||
(tableRef.current?.clientWidth ?? 0)
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const rFlag = scrollOffset !== containerWidth;
|
||||
const lFlag = scrollOffset > 0;
|
||||
setScrollHandle((pre) => ({ ...pre, right: rFlag, left: lFlag }));
|
||||
}, [scrollOffset, containerWidth]);
|
||||
|
||||
return (
|
||||
<div className="tw-table-responsive">
|
||||
{sampleData?.rows?.length && sampleData?.columns?.length ? (
|
||||
<table
|
||||
className="tw-min-w-max tw-w-full tw-table-auto"
|
||||
data-testid="sample-data-table">
|
||||
<thead>
|
||||
<tr className="tableHead-row">
|
||||
{sampleData.columns.map((column) => {
|
||||
<div
|
||||
className="tw-relative tw-flex tw-justify-between"
|
||||
onScrollCapture={() => {
|
||||
setScrollOffSet(tableRef.current?.scrollLeft ?? 0);
|
||||
}}>
|
||||
{scrollHandle.left ? (
|
||||
<button
|
||||
className="tw-border tw-border-main tw-fixed tw-left-7 tw-top-2/3 tw-rounded-full tw-shadow-md tw-z-50 tw-bg-body-main tw-w-8 tw-h-8"
|
||||
onClick={() => scrollHandler(-50)}>
|
||||
<i className="fas fa-chevron-left tw-text-grey-muted" />
|
||||
</button>
|
||||
) : null}
|
||||
{scrollHandle.right ? (
|
||||
<button
|
||||
className="tw-border tw-border-main tw-fixed tw-right-7 tw-top-2/3 tw-rounded-full tw-shadow-md tw-z-50 tw-bg-body-main tw-w-8 tw-h-8"
|
||||
onClick={() => scrollHandler(50)}>
|
||||
<i className="fas fa-chevron-right tw-text-grey-muted" />
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<div className="tw-table-responsive" ref={tableRef}>
|
||||
{sampleData?.rows?.length && sampleData?.columns?.length ? (
|
||||
<table
|
||||
className="tw-min-w-max tw-w-full tw-table-auto"
|
||||
data-testid="sample-data-table">
|
||||
<thead>
|
||||
<tr className="tableHead-row">
|
||||
{sampleData.columns.map((column) => {
|
||||
return (
|
||||
<th
|
||||
className="tableHead-cell"
|
||||
data-testid="column-name"
|
||||
key={column.name}>
|
||||
{column.name}
|
||||
<span className="tw-py-0.5 tw-px-1 tw-ml-1 tw-rounded tw-text-grey-muted">
|
||||
({lowerCase(column.dataType)})
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="tw-text-gray-600 tw-text-sm">
|
||||
{sampleData?.rows?.map((row, rowIndex) => {
|
||||
return (
|
||||
<th
|
||||
className="tableHead-cell"
|
||||
data-testid="column-name"
|
||||
key={column.name}>
|
||||
{column.name}
|
||||
<span className="tw-py-0.5 tw-px-1 tw-ml-1 tw-rounded tw-text-grey-muted">
|
||||
({lowerCase(column.dataType)})
|
||||
</span>
|
||||
</th>
|
||||
<tr
|
||||
className={classNames(
|
||||
'tableBody-row',
|
||||
!isEven(rowIndex + 1) ? 'odd-row' : null
|
||||
)}
|
||||
data-testid="row"
|
||||
key={rowIndex}>
|
||||
{row.map((data, index) => {
|
||||
return (
|
||||
<td
|
||||
className="tableBody-cell"
|
||||
data-testid="cell"
|
||||
key={index}>
|
||||
{data ? data.toString() : '--'}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="tw-text-gray-600 tw-text-sm">
|
||||
{sampleData?.rows?.map((row, rowIndex) => {
|
||||
return (
|
||||
<tr
|
||||
className={classNames(
|
||||
'tableBody-row',
|
||||
!isEven(rowIndex + 1) ? 'odd-row' : null
|
||||
)}
|
||||
data-testid="row"
|
||||
key={rowIndex}>
|
||||
{row.map((data, index) => {
|
||||
return (
|
||||
<td
|
||||
className="tableBody-cell"
|
||||
data-testid="cell"
|
||||
key={index}>
|
||||
{data ? data.toString() : '--'}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="tw-flex tw-justify-center tw-font-medium tw-items-center tw-border tw-border-main tw-rounded-md tw-p-8">
|
||||
No sample data available
|
||||
</div>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="tw-flex tw-justify-center tw-font-medium tw-items-center tw-border tw-border-main tw-rounded-md tw-p-8">
|
||||
No sample data available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -11,11 +11,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isNil, isUndefined, lowerCase } from 'lodash';
|
||||
import { DatasetSchemaTableTab } from 'Models';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router';
|
||||
import { getDatasetTabPath } from '../../constants/constants';
|
||||
import { lowerCase } from 'lodash';
|
||||
import React, { Fragment, FunctionComponent, useState } from 'react';
|
||||
import {
|
||||
ColumnJoins,
|
||||
Table,
|
||||
@ -23,9 +20,6 @@ import {
|
||||
} from '../../generated/entity/data/table';
|
||||
import Searchbar from '../common/searchbar/Searchbar';
|
||||
import EntityTable from '../EntityTable/EntityTable.component';
|
||||
import SampleDataTable, {
|
||||
SampleColumns,
|
||||
} from '../SampleDataTable/SampleDataTable.component';
|
||||
|
||||
type Props = {
|
||||
owner?: Table['owner'];
|
||||
@ -42,127 +36,46 @@ const SchemaTab: FunctionComponent<Props> = ({
|
||||
columns,
|
||||
joins,
|
||||
onUpdate,
|
||||
sampleData,
|
||||
columnName,
|
||||
hasEditAccess,
|
||||
owner,
|
||||
isReadOnly = false,
|
||||
}: Props) => {
|
||||
const history = useHistory();
|
||||
const { datasetFQN: tableFQN, tab } = useParams() as Record<string, string>;
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [checkedValue, setCheckedValue] =
|
||||
useState<DatasetSchemaTableTab>('schema');
|
||||
|
||||
const handleSearchAction = (searchValue: string) => {
|
||||
setSearchText(searchValue);
|
||||
};
|
||||
|
||||
const handleToggleChange = (value: DatasetSchemaTableTab) => {
|
||||
setCheckedValue(value);
|
||||
history.push({
|
||||
pathname: getDatasetTabPath(tableFQN, value),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (tab && ['schema', 'sample_data'].includes(tab)) {
|
||||
const activeTab = isUndefined(tab)
|
||||
? 'schema'
|
||||
: (tab as DatasetSchemaTableTab);
|
||||
setCheckedValue(activeTab);
|
||||
}
|
||||
}, [tab]);
|
||||
|
||||
const getToggleButtonClasses = (type: string): string => {
|
||||
return (
|
||||
'tw-flex-1 tw-font-medium tw-border tw-border-transparent tw-rounded tw-py-1 tw-px-2 focus:tw-outline-none' +
|
||||
(type === checkedValue
|
||||
? ' tw-bg-primary tw-text-white tw-border-main'
|
||||
: '')
|
||||
);
|
||||
};
|
||||
|
||||
const getSampleDataWithType = () => {
|
||||
const updatedColumns = sampleData?.columns?.map((column) => {
|
||||
const matchedColumn = columns.find((col) => col.name === column);
|
||||
|
||||
if (matchedColumn) {
|
||||
return {
|
||||
name: matchedColumn.name,
|
||||
dataType: matchedColumn.dataType,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: column,
|
||||
dataType: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
columns: updatedColumns as SampleColumns[] | undefined,
|
||||
rows: sampleData?.rows,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Fragment>
|
||||
<div className="tw-grid tw-grid-cols-3 tw-gap-x-2">
|
||||
<div>
|
||||
{checkedValue === 'schema' && (
|
||||
<Searchbar
|
||||
placeholder="Find in table..."
|
||||
searchValue={searchText}
|
||||
typingInterval={1500}
|
||||
onSearch={handleSearchAction}
|
||||
/>
|
||||
)}
|
||||
<Searchbar
|
||||
placeholder="Find in table..."
|
||||
searchValue={searchText}
|
||||
typingInterval={1500}
|
||||
onSearch={handleSearchAction}
|
||||
/>
|
||||
</div>
|
||||
{!isReadOnly && !isNil(sampleData) ? (
|
||||
<div className="tw-col-span-2 tw-text-right tw-mb-4">
|
||||
<div
|
||||
className="tw-w-60 tw-inline-flex tw-border tw-border-main
|
||||
tw-text-sm tw-rounded-md tw-h-8 tw-bg-white">
|
||||
<button
|
||||
className={getToggleButtonClasses('schema')}
|
||||
data-testid="schema-button"
|
||||
onClick={() => handleToggleChange('schema')}>
|
||||
Schema
|
||||
</button>
|
||||
<button
|
||||
className={getToggleButtonClasses('sample_data')}
|
||||
data-testid="sample-data-button"
|
||||
onClick={() => {
|
||||
handleToggleChange('sample_data');
|
||||
}}>
|
||||
Sample Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="row">
|
||||
{columns?.length > 0 ? (
|
||||
<div className="col-sm-12">
|
||||
{checkedValue === 'schema' ? (
|
||||
<EntityTable
|
||||
columnName={columnName}
|
||||
hasEditAccess={Boolean(hasEditAccess)}
|
||||
isReadOnly={isReadOnly}
|
||||
joins={joins}
|
||||
owner={owner}
|
||||
searchText={lowerCase(searchText)}
|
||||
tableColumns={columns}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
) : (
|
||||
<SampleDataTable sampleData={getSampleDataWithType()} />
|
||||
)}
|
||||
<EntityTable
|
||||
columnName={columnName}
|
||||
hasEditAccess={Boolean(hasEditAccess)}
|
||||
isReadOnly={isReadOnly}
|
||||
joins={joins}
|
||||
owner={owner}
|
||||
searchText={lowerCase(searchText)}
|
||||
tableColumns={columns}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -28,6 +28,10 @@ export const datasetTableTabs = [
|
||||
name: 'DBT',
|
||||
path: 'dbt',
|
||||
},
|
||||
{
|
||||
name: 'Sample Data',
|
||||
path: 'sample_data',
|
||||
},
|
||||
{
|
||||
name: 'Manage',
|
||||
path: 'manage',
|
||||
@ -50,13 +54,17 @@ export const getCurrentDatasetTab = (tab: string) => {
|
||||
|
||||
break;
|
||||
|
||||
case 'manage':
|
||||
case 'sample_data':
|
||||
currentTab = 5;
|
||||
|
||||
break;
|
||||
|
||||
case 'manage':
|
||||
currentTab = 6;
|
||||
|
||||
break;
|
||||
|
||||
case 'schema':
|
||||
case 'sample_data':
|
||||
default:
|
||||
currentTab = 1;
|
||||
|
||||
|
@ -101,6 +101,8 @@ import IconVersionBlack from '../assets/svg/version-black.svg';
|
||||
import IconVersionWhite from '../assets/svg/version-white.svg';
|
||||
import IconVersion from '../assets/svg/version.svg';
|
||||
import IconWarning from '../assets/svg/warning.svg';
|
||||
import IconSampleDataColor from '../assets/svg/sample-data-colored.svg';
|
||||
import IconSampleData from '../assets/svg/sample-data.svg';
|
||||
|
||||
type Props = {
|
||||
alt: string;
|
||||
@ -199,6 +201,8 @@ export const Icons = {
|
||||
PROFILERCOLOR: 'icon-profilercolor',
|
||||
MANAGECOLOR: 'icon-managecolor',
|
||||
SEARCHV1COLOR: 'icon-searchv1color',
|
||||
SAMPLE_DATA: 'sample-data',
|
||||
SAMPLE_DATA_COLOR: 'sample-data-color',
|
||||
};
|
||||
|
||||
const SVGIcons: FunctionComponent<Props> = ({
|
||||
@ -568,6 +572,14 @@ const SVGIcons: FunctionComponent<Props> = ({
|
||||
case Icons.SEARCHV1COLOR:
|
||||
IconComponent = IconSearchV1Color;
|
||||
|
||||
break;
|
||||
case Icons.SAMPLE_DATA:
|
||||
IconComponent = IconSampleData;
|
||||
|
||||
break;
|
||||
case Icons.SAMPLE_DATA_COLOR:
|
||||
IconComponent = IconSampleDataColor;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
Loading…
x
Reference in New Issue
Block a user