mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-10-05 06:18:04 +03:00
console: UX improvement for postgres tracking UI
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9578 Co-authored-by: Matthew Goodwin <49927862+m4ttheweric@users.noreply.github.com> GitOrigin-RevId: 334a08a89a22196e8e5d11b469279690d279ed46
This commit is contained in:
parent
324e02b43e
commit
ef79d6be9f
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
@ -56,6 +57,7 @@ import { createNewSchema, deleteCurrentSchema } from './Actions';
|
||||
import { EmptyState } from './components/EmptyState/EmptyState';
|
||||
import { TrackableFunctionsList } from './FunctionsList';
|
||||
import { getTrackableFunctions } from './utils';
|
||||
import { FeatureFlagContainer } from './TrackTablesContainer';
|
||||
|
||||
const DeleteSchemaButton = ({ dispatch, migrationMode, currentDataSource }) => {
|
||||
const successCb = () => {
|
||||
@ -737,9 +739,18 @@ class Schema extends Component {
|
||||
<hr className="my-md" />
|
||||
{getCurrentSchemaSection()}
|
||||
<hr className="my-md" />
|
||||
{getUntrackedTablesSection()}
|
||||
|
||||
<FeatureFlagContainer
|
||||
dataSourceName={currentDataSource}
|
||||
schema={currentSchema}
|
||||
dispatch={dispatch}
|
||||
>
|
||||
{getUntrackedTablesSection()}
|
||||
</FeatureFlagContainer>
|
||||
|
||||
{isFeatureSupported('tables.relationships.track') &&
|
||||
getUntrackedRelationsSection()}
|
||||
|
||||
{getUntrackedFunctionsSection(
|
||||
isFeatureSupported('functions.track.enabled')
|
||||
)}
|
||||
|
@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import { TrackableResourceTabs } from '../../../../features/Data/ManageDatabase/components';
|
||||
import { CollapsibleResource } from '../../../../features/Data/ManageDatabase/parts';
|
||||
import { TableList } from '../../../../features/Data/ManageTable/parts/TableList';
|
||||
import { PostgresTable } from '../../../../features/DataSource';
|
||||
import {
|
||||
availableFeatureFlagIds,
|
||||
useIsFeatureFlagEnabled,
|
||||
} from '../../../../features/FeatureFlags';
|
||||
import { exportMetadata } from '../../../../metadata/actions';
|
||||
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
|
||||
import {
|
||||
getSchemaBaseRoute,
|
||||
getTableModifyRoute,
|
||||
} from '../../../Common/utils/routesUtils';
|
||||
import { updateSchemaInfo } from '../DataActions';
|
||||
import _push from '../push';
|
||||
import { ExperimentalFeatureBanner } from '../../../../features/components';
|
||||
import { useTrackTablesState } from './useTrackTablesState';
|
||||
|
||||
export const TrackTablesContainer = ({
|
||||
dataSourceName,
|
||||
schema,
|
||||
dispatch,
|
||||
}: {
|
||||
dataSourceName: string;
|
||||
schema: string;
|
||||
dispatch: any;
|
||||
}) => {
|
||||
const {
|
||||
trackedTables,
|
||||
untrackedTables,
|
||||
tab,
|
||||
setTab,
|
||||
introspectionError,
|
||||
isIntroLoading,
|
||||
isIntrospectionError,
|
||||
isMetaLoading,
|
||||
isMetadataError,
|
||||
metadataError,
|
||||
} = useTrackTablesState(dataSourceName, schema);
|
||||
|
||||
const onMultiple = React.useCallback(() => {
|
||||
dispatch(exportMetadata()).then(
|
||||
dispatch(updateSchemaInfo()).then(() => {
|
||||
dispatch(_push(getSchemaBaseRoute(schema, dataSourceName)));
|
||||
})
|
||||
);
|
||||
}, [dataSourceName, dispatch, schema]);
|
||||
|
||||
if (isMetadataError || isIntrospectionError)
|
||||
return (
|
||||
<IndicatorCard
|
||||
status="negative"
|
||||
headline="Error while fetching data"
|
||||
showIcon
|
||||
>
|
||||
<div>
|
||||
{metadataError?.message ??
|
||||
(introspectionError as any)?.message ??
|
||||
'Something went wrong'}
|
||||
</div>
|
||||
</IndicatorCard>
|
||||
);
|
||||
|
||||
return (
|
||||
<CollapsibleResource
|
||||
title={<div>Untracked tables or views</div>}
|
||||
tooltip="Tables or views that are not exposed over the GraphQL API"
|
||||
defaultOpen
|
||||
key={`${dataSourceName}-${schema}`}
|
||||
>
|
||||
<ExperimentalFeatureBanner
|
||||
githubIssueLink={
|
||||
'https://github.com/hasura/graphql-engine/discussions/9727'
|
||||
}
|
||||
/>
|
||||
<div className="my-2 text-muted">
|
||||
Tracking tables adds them to your GraphQL API. All objects will be
|
||||
admin-only until permissions have been set.
|
||||
</div>
|
||||
<TrackableResourceTabs
|
||||
value={tab}
|
||||
onValueChange={value => {
|
||||
setTab(value);
|
||||
}}
|
||||
isLoading={isMetaLoading || isIntroLoading}
|
||||
items={{
|
||||
untracked: {
|
||||
amount: untrackedTables.length,
|
||||
content: (
|
||||
<TableList
|
||||
viewingTablesThatAre={'untracked'}
|
||||
dataSourceName={dataSourceName}
|
||||
tables={untrackedTables}
|
||||
onMultipleTablesTrack={onMultiple}
|
||||
onSingleTableTrack={table => {
|
||||
dispatch(
|
||||
_push(
|
||||
getTableModifyRoute(
|
||||
schema,
|
||||
dataSourceName,
|
||||
(table.table as PostgresTable).name,
|
||||
table.type === 'BASE TABLE'
|
||||
)
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
tracked: {
|
||||
amount: trackedTables.length,
|
||||
content: (
|
||||
<TableList
|
||||
viewingTablesThatAre={'tracked'}
|
||||
dataSourceName={dataSourceName}
|
||||
tables={trackedTables}
|
||||
onMultipleTablesTrack={onMultiple}
|
||||
onSingleTableTrack={table => {
|
||||
onMultiple();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</CollapsibleResource>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeatureFlagContainer = ({
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
dataSourceName: string;
|
||||
schema: string;
|
||||
dispatch: any;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { enabled } = useIsFeatureFlagEnabled(
|
||||
availableFeatureFlagIds.trackingSectionUI
|
||||
);
|
||||
|
||||
if (enabled) return <TrackTablesContainer {...props} />;
|
||||
|
||||
return children;
|
||||
};
|
@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
adaptTrackedTables,
|
||||
adaptUntrackedTables,
|
||||
selectTrackedTables,
|
||||
} from '../../../../features/Data/ManageTable/selectors';
|
||||
import { useIntrospectedTables } from '../../../../features/Data/hooks/useIntrospectedTables';
|
||||
import { Feature, IntrospectedTable } from '../../../../features/DataSource';
|
||||
import { useMetadata } from '../../../../features/hasura-metadata-api';
|
||||
|
||||
export function useTrackTablesState(dataSourceName: string, schema: string) {
|
||||
const [tab, setTab] = React.useState<'tracked' | 'untracked'>('untracked');
|
||||
|
||||
const {
|
||||
data: metadataTables = [],
|
||||
isFetched,
|
||||
isLoading: isMetaLoading,
|
||||
isError: isMetadataError,
|
||||
error: metadataError,
|
||||
} = useMetadata(m => selectTrackedTables(m)(dataSourceName));
|
||||
|
||||
// if this is not memoized it re-runs each render
|
||||
const select = React.useCallback(
|
||||
(introspectedTables: Feature | IntrospectedTable[]) => {
|
||||
const untracked =
|
||||
adaptUntrackedTables(metadataTables)(introspectedTables);
|
||||
const untrackedBySchema = untracked.filter(
|
||||
t => t.name.split('.')[0] === schema
|
||||
);
|
||||
const tracked = adaptTrackedTables(metadataTables)(introspectedTables);
|
||||
const trackedBySchema = tracked.filter(
|
||||
t => t.name.split('.')[0] === schema
|
||||
);
|
||||
return {
|
||||
untrackedTables: untrackedBySchema,
|
||||
trackedTables: trackedBySchema,
|
||||
};
|
||||
},
|
||||
[metadataTables, schema]
|
||||
);
|
||||
|
||||
const {
|
||||
data: { untrackedTables = [], trackedTables = [] } = {},
|
||||
isLoading: isIntroLoading,
|
||||
isError: isIntrospectionError,
|
||||
error: introspectionError,
|
||||
} = useIntrospectedTables({
|
||||
dataSourceName,
|
||||
options: {
|
||||
select,
|
||||
enabled: isFetched,
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: data => {
|
||||
if (data?.untrackedTables.length === 0) {
|
||||
// if user has no tracked tables, switch to the untracked list
|
||||
setTab('tracked');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
untrackedTables,
|
||||
trackedTables,
|
||||
isMetaLoading,
|
||||
isMetadataError,
|
||||
metadataError,
|
||||
isIntroLoading,
|
||||
isIntrospectionError,
|
||||
introspectionError,
|
||||
tab,
|
||||
setTab,
|
||||
};
|
||||
}
|
@ -22,12 +22,14 @@ export type CustomFieldNamesFormProps = {
|
||||
callToAction?: string;
|
||||
callToActionLoadingText?: string;
|
||||
callToDeny?: string;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export const CustomFieldNamesForm: React.VFC<
|
||||
CustomFieldNamesFormProps
|
||||
> = props => {
|
||||
const {
|
||||
isLoading,
|
||||
onClose,
|
||||
callToAction = 'Save',
|
||||
callToActionLoadingText = 'Saving...',
|
||||
@ -158,6 +160,7 @@ export const CustomFieldNamesForm: React.VFC<
|
||||
*/}
|
||||
<Dialog.Footer
|
||||
callToAction={callToAction}
|
||||
isLoading={isLoading}
|
||||
callToActionLoadingText={callToActionLoadingText}
|
||||
callToDeny={callToDeny}
|
||||
onClose={onClose}
|
||||
|
@ -7,23 +7,21 @@ import { IconTooltip } from '../../../../new-components/Tooltip';
|
||||
|
||||
export const CollapsibleResource: React.FC<
|
||||
{
|
||||
title: string;
|
||||
title: React.ReactNode;
|
||||
tooltip: string;
|
||||
} & Omit<CollapsibleProps, 'triggerChildren'>
|
||||
> = ({ title, tooltip, children, ...rest }) => (
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div>
|
||||
<div className="flex mb-1 items-center">
|
||||
<div className="font-semibold inline-flex items-center text-lg">
|
||||
{title}
|
||||
</div>
|
||||
<IconTooltip
|
||||
icon={<RiInformationFill />}
|
||||
message={tooltip}
|
||||
side="right"
|
||||
/>
|
||||
<div className="flex mb-1 items-center">
|
||||
<div className="font-semibold inline-flex items-center text-lg">
|
||||
{title}
|
||||
</div>
|
||||
<IconTooltip
|
||||
icon={<RiInformationFill />}
|
||||
message={tooltip}
|
||||
side="right"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{...rest}
|
||||
|
@ -80,7 +80,7 @@ export const ManageTrackedTables = ({
|
||||
amount: untrackedTables.length,
|
||||
content: (
|
||||
<TableList
|
||||
mode={'untrack'}
|
||||
viewingTablesThatAre={'untracked'}
|
||||
dataSourceName={dataSourceName}
|
||||
tables={untrackedTables}
|
||||
/>
|
||||
@ -90,7 +90,7 @@ export const ManageTrackedTables = ({
|
||||
amount: trackedTables.length,
|
||||
content: (
|
||||
<TableList
|
||||
mode={'track'}
|
||||
viewingTablesThatAre={'tracked'}
|
||||
dataSourceName={dataSourceName}
|
||||
tables={trackedTables}
|
||||
/>
|
||||
|
@ -4,11 +4,12 @@ import { Input } from '../../../../new-components/Form';
|
||||
|
||||
type SearchBarProps = {
|
||||
onSearch: (searchText: string) => void;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
export const SearchBar = ({ onSearch }: SearchBarProps) => {
|
||||
export const SearchBar = ({ onSearch, defaultValue }: SearchBarProps) => {
|
||||
const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [value, setValue] = React.useState('');
|
||||
const [value, setValue] = React.useState(defaultValue ?? '');
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
|
@ -1,102 +1,144 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||
import { Badge } from '../../../../new-components/Badge';
|
||||
import { FaFilter } from 'react-icons/fa';
|
||||
import { DropDown } from '../../../../new-components/AdvancedDropDown';
|
||||
import { Button } from '../../../../new-components/Button';
|
||||
import { CardedTable } from '../../../../new-components/CardedTable';
|
||||
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
|
||||
import {
|
||||
DEFAULT_PAGE_NUMBER,
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_PAGE_SIZES,
|
||||
} from '../../TrackResources/constants';
|
||||
import { useCheckRows } from '../hooks/useCheckRows';
|
||||
import { TrackableTable } from '../types';
|
||||
import { paginate, search } from '../utils';
|
||||
import { SearchBar } from './SearchBar';
|
||||
import { TableRow } from './TableRow';
|
||||
import { usePushRoute } from '../../../ConnectDBRedesign/hooks';
|
||||
import { useTrackTables } from '../../hooks/useTrackTables';
|
||||
import { hasuraToast } from '../../../../new-components/Toasts';
|
||||
import { usePushRoute } from '../../../ConnectDBRedesign/hooks';
|
||||
import { PostgresTable } from '../../../DataSource';
|
||||
import { TrackableListMenu } from '../../TrackResources/components/TrackableListMenu';
|
||||
import { usePaginatedSearchableList } from '../../TrackResources/hooks';
|
||||
import { DisplayToastErrorMessage } from '../../components/DisplayErrorMessage';
|
||||
import { useTrackTables } from '../../hooks/useTrackTables';
|
||||
import { TrackableTable } from '../types';
|
||||
import { filterByTableType, filterByText } from '../utils';
|
||||
import { TableRow } from './TableRow';
|
||||
|
||||
interface TableListProps {
|
||||
dataSourceName: string;
|
||||
tables: TrackableTable[];
|
||||
mode: 'track' | 'untrack';
|
||||
viewingTablesThatAre: 'tracked' | 'untracked';
|
||||
onTrackedTable?: () => void;
|
||||
onMultipleTablesTrack?: () => void;
|
||||
defaultFilter?: string;
|
||||
onSingleTableTrack?: (table: TrackableTable) => void;
|
||||
}
|
||||
|
||||
// const getDefaultSelectedTableType = (availableTableTypes: string[]) => {
|
||||
// return availableTableTypes.includes('BASE TABLE')
|
||||
// ? ['BASE TABLE']
|
||||
// : availableTableTypes;
|
||||
// };
|
||||
|
||||
const countByType = (tables: TrackableTable[]) =>
|
||||
tables.reduce<Record<string, number>>((prev, current) => {
|
||||
if (prev[current.type]) {
|
||||
prev[current.type]++;
|
||||
} else {
|
||||
prev[current.type] = 1;
|
||||
}
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
export const TableList = (props: TableListProps) => {
|
||||
const { mode, dataSourceName, tables, onTrackedTable } = props;
|
||||
const {
|
||||
viewingTablesThatAre,
|
||||
dataSourceName,
|
||||
tables,
|
||||
onTrackedTable,
|
||||
defaultFilter,
|
||||
onMultipleTablesTrack,
|
||||
} = props;
|
||||
|
||||
const [pageNumber, setPageNumber] = useState(DEFAULT_PAGE_NUMBER);
|
||||
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const filteredTables = search(tables, searchText);
|
||||
//const availableTableTypes = getUniqueTableTypes(tables);
|
||||
|
||||
const checkboxRef = React.useRef<HTMLInputElement>(null);
|
||||
const typeCounts = React.useMemo(() => countByType(tables), [tables]);
|
||||
|
||||
const { checkedIds, onCheck, allChecked, toggleAll, reset, inputStatus } =
|
||||
useCheckRows(filteredTables || []);
|
||||
const availableTableTypes = React.useMemo(
|
||||
() => Object.keys(typeCounts),
|
||||
[typeCounts]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!checkboxRef.current) return;
|
||||
checkboxRef.current.indeterminate = inputStatus === 'indeterminate';
|
||||
}, [inputStatus]);
|
||||
const [selectedTableTypes, setSelectedTableTypes] = useState<string[]>([]);
|
||||
|
||||
const searchFn = React.useCallback(
|
||||
(searchText: string, table: TrackableTable) => {
|
||||
const parentText = table.name.toLowerCase().split('.').join(' / ');
|
||||
return (
|
||||
filterByText(parentText, searchText) &&
|
||||
filterByTableType(table.type, selectedTableTypes)
|
||||
);
|
||||
},
|
||||
[selectedTableTypes]
|
||||
);
|
||||
const listProps = usePaginatedSearchableList<TrackableTable>({
|
||||
data: tables,
|
||||
filterFn: searchFn,
|
||||
defaultQuery: defaultFilter,
|
||||
});
|
||||
|
||||
const {
|
||||
checkData: { onCheck, checkedIds, reset: resetCheckboxes, checkAllElement },
|
||||
paginatedData: paginatedTables,
|
||||
checkedItems: checkedTables,
|
||||
} = listProps;
|
||||
|
||||
const { trackTables, isLoading, untrackTables } = useTrackTables({
|
||||
dataSourceName,
|
||||
});
|
||||
|
||||
const onClick = async () => {
|
||||
const tables = filteredTables.filter(({ name }) =>
|
||||
checkedIds.includes(name)
|
||||
);
|
||||
const verb = viewingTablesThatAre === 'untracked' ? 'tracked' : 'untracked';
|
||||
const action =
|
||||
viewingTablesThatAre === 'untracked' ? trackTables : untrackTables;
|
||||
|
||||
if (mode === 'untrack') {
|
||||
trackTables({
|
||||
tablesToBeTracked: tables,
|
||||
onSuccess: () => {
|
||||
hasuraToast({
|
||||
type: 'success',
|
||||
title: 'Successfully tracked',
|
||||
message: `${tables.length} ${
|
||||
tables.length <= 1 ? 'table' : 'tables'
|
||||
} tracked!`,
|
||||
});
|
||||
},
|
||||
onError: err => {
|
||||
hasuraToast({
|
||||
type: 'error',
|
||||
title: err.name,
|
||||
children: <DisplayToastErrorMessage message={err.message} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
untrackTables({
|
||||
tablesToBeUntracked: tables,
|
||||
onSuccess: () => {
|
||||
hasuraToast({
|
||||
type: 'success',
|
||||
title: 'Successfully untracked',
|
||||
message: `${tables.length} ${
|
||||
tables.length <= 1 ? 'table' : 'tables'
|
||||
} untracked`,
|
||||
});
|
||||
},
|
||||
onError: err => {
|
||||
hasuraToast({
|
||||
type: 'error',
|
||||
title: err.name,
|
||||
children: <DisplayToastErrorMessage message={err.message} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
const handleCheckAction = async () => {
|
||||
//make a copy of the current counts to have an accurate copy of what it was prior to the track/untrack
|
||||
const currentCounts = { ...typeCounts };
|
||||
// count the items by type in the payload
|
||||
const actionCounts = countByType(checkedTables);
|
||||
|
||||
onTrackedTable?.();
|
||||
reset();
|
||||
action({
|
||||
tables: checkedTables,
|
||||
onSuccess: () => {
|
||||
// create an array of item types where the number tracked/untracked is the same as the total (user tracked/untracked ALL of that type)
|
||||
const toRemove = Object.entries(actionCounts).reduce<string[]>(
|
||||
(prev, [key, value]) => {
|
||||
if (value === currentCounts[key]) {
|
||||
prev = [...prev, key];
|
||||
}
|
||||
return prev;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// if we found any, filter them out of the selectedTableTypes
|
||||
if (toRemove.length > 0) {
|
||||
setSelectedTableTypes(prev =>
|
||||
prev.filter(t => !toRemove.includes(t))
|
||||
);
|
||||
}
|
||||
|
||||
onTrackedTable?.();
|
||||
resetCheckboxes();
|
||||
|
||||
hasuraToast({
|
||||
type: 'success',
|
||||
title: `Successfully ${verb}`,
|
||||
message: `${checkedTables.length} ${
|
||||
checkedTables.length <= 1 ? 'table' : 'tables'
|
||||
} ${verb}!`,
|
||||
});
|
||||
onMultipleTablesTrack?.();
|
||||
},
|
||||
onError: err => {
|
||||
hasuraToast({
|
||||
type: 'error',
|
||||
title: err.name,
|
||||
children: <DisplayToastErrorMessage message={err.message} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const pushRoute = usePushRoute();
|
||||
@ -105,104 +147,118 @@ export const TableList = (props: TableListProps) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<IndicatorCard>{`No ${
|
||||
mode === 'track' ? 'tracked' : 'untracked'
|
||||
viewingTablesThatAre === 'tracked' ? 'tracked' : 'untracked'
|
||||
} tables found`}</IndicatorCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between space-x-4">
|
||||
<div className="flex gap-5">
|
||||
<Button
|
||||
mode="primary"
|
||||
disabled={!checkedIds.length}
|
||||
onClick={onClick}
|
||||
isLoading={isLoading}
|
||||
loadingText={'Please Wait'}
|
||||
<TrackableListMenu
|
||||
checkActionText={`${
|
||||
viewingTablesThatAre === 'tracked' ? 'Untrack' : 'Track'
|
||||
} Selected (${checkedTables.length})`}
|
||||
handleTrackButton={() => {
|
||||
handleCheckAction();
|
||||
}}
|
||||
showButton
|
||||
isLoading={isLoading}
|
||||
searchChildren={
|
||||
<DropDown.Root
|
||||
trigger={
|
||||
<Button icon={<FaFilter />}>
|
||||
{selectedTableTypes.length ? (
|
||||
<>Type ({selectedTableTypes.length} selected)</>
|
||||
) : (
|
||||
<>No Filters applied</>
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{`${mode === 'track' ? 'Untrack' : 'Track'} Selected (${
|
||||
checkedIds.length
|
||||
})`}
|
||||
</Button>
|
||||
<span className="border-r border-slate-300"></span>
|
||||
<div className="flex gap-2">
|
||||
<SearchBar onSearch={data => setSearchText(data)} />
|
||||
{searchText.length ? (
|
||||
<Badge>{filteredTables.length} results found</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
icon={<FaAngleLeft />}
|
||||
onClick={() => setPageNumber(pageNumber - 1)}
|
||||
disabled={pageNumber === 1}
|
||||
/>
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value));
|
||||
}}
|
||||
className="block w-full max-w-xl h-8 min-h-full shadow-sm rounded pl-3 pr-6 py-0.5 border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400"
|
||||
>
|
||||
{DEFAULT_PAGE_SIZES.map(_pageSize => (
|
||||
<option key={_pageSize} value={_pageSize}>
|
||||
Show {_pageSize} tables
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
icon={<FaAngleRight />}
|
||||
onClick={() => setPageNumber(pageNumber + 1)}
|
||||
disabled={pageNumber >= filteredTables.length / pageSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CardedTable.Table>
|
||||
<CardedTable.TableHead>
|
||||
<CardedTable.TableHeadRow>
|
||||
<th className="w-0 bg-gray-50 px-sm text-sm font-semibold text-muted uppercase tracking-wider border-r">
|
||||
<input
|
||||
ref={checkboxRef}
|
||||
type="checkbox"
|
||||
className="cursor-pointer
|
||||
rounded border shadow-sm border-gray-400 hover:border-gray-500 focus:ring-yellow-400"
|
||||
checked={allChecked}
|
||||
onChange={toggleAll}
|
||||
/>
|
||||
</th>
|
||||
<CardedTable.TableHeadCell>Object</CardedTable.TableHeadCell>
|
||||
<CardedTable.TableHeadCell>Type</CardedTable.TableHeadCell>
|
||||
<CardedTable.TableHeadCell>Actions</CardedTable.TableHeadCell>
|
||||
</CardedTable.TableHeadRow>
|
||||
</CardedTable.TableHead>
|
||||
|
||||
<CardedTable.TableBody>
|
||||
{paginate(filteredTables, pageSize, pageNumber).map(table => (
|
||||
<TableRow
|
||||
key={table.id}
|
||||
table={table}
|
||||
dataSourceName={dataSourceName}
|
||||
checked={checkedIds.includes(table.id)}
|
||||
reset={reset}
|
||||
onChange={() => onCheck(table.id)}
|
||||
onTableNameClick={
|
||||
mode === 'track'
|
||||
? () => {
|
||||
pushRoute(
|
||||
`data/v2/manage/table/browse?database=${dataSourceName}&table=${encodeURIComponent(
|
||||
JSON.stringify(table.table)
|
||||
)}`
|
||||
<DropDown.Label>Table Types:</DropDown.Label>
|
||||
<>
|
||||
{availableTableTypes.map(tableType => (
|
||||
<DropDown.CheckItem
|
||||
key={tableType}
|
||||
onCheckChange={() => {
|
||||
if (selectedTableTypes.includes(tableType))
|
||||
setSelectedTableTypes(t =>
|
||||
t.filter(x => x !== tableType)
|
||||
);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</CardedTable.TableBody>
|
||||
</CardedTable.Table>
|
||||
else setSelectedTableTypes(t => [...t, tableType]);
|
||||
}}
|
||||
checked={selectedTableTypes.includes(tableType)}
|
||||
>
|
||||
<div>
|
||||
{tableType} ({typeCounts[tableType]})
|
||||
</div>
|
||||
</DropDown.CheckItem>
|
||||
))}
|
||||
</>
|
||||
</DropDown.Root>
|
||||
}
|
||||
{...listProps}
|
||||
/>
|
||||
|
||||
{!paginatedTables.length ? (
|
||||
<div className="space-y-4">
|
||||
<IndicatorCard>{`No ${
|
||||
viewingTablesThatAre === 'tracked' ? 'tracked' : 'untracked'
|
||||
} tables found found for the applied filter`}</IndicatorCard>
|
||||
</div>
|
||||
) : (
|
||||
<CardedTable.Table>
|
||||
<CardedTable.TableHead>
|
||||
<CardedTable.TableHeadRow>
|
||||
<th className="w-0 bg-gray-50 px-sm text-sm font-semibold text-muted uppercase tracking-wider border-r">
|
||||
{checkAllElement()}
|
||||
</th>
|
||||
<CardedTable.TableHeadCell>Table</CardedTable.TableHeadCell>
|
||||
<CardedTable.TableHeadCell>Type</CardedTable.TableHeadCell>
|
||||
<CardedTable.TableHeadCell>Actions</CardedTable.TableHeadCell>
|
||||
</CardedTable.TableHeadRow>
|
||||
</CardedTable.TableHead>
|
||||
|
||||
<CardedTable.TableBody>
|
||||
{paginatedTables.map(table => (
|
||||
<TableRow
|
||||
key={table.id}
|
||||
table={table}
|
||||
dataSourceName={dataSourceName}
|
||||
checked={checkedIds.includes(table.id)}
|
||||
reset={resetCheckboxes}
|
||||
onChange={() => onCheck(table.id)}
|
||||
onTableTrack={props.onSingleTableTrack}
|
||||
onTableNameClick={
|
||||
viewingTablesThatAre === 'tracked'
|
||||
? () => {
|
||||
if ('schema' in (table.table as any)) {
|
||||
const { name, schema } = table.table as PostgresTable;
|
||||
|
||||
pushRoute(
|
||||
`/data/${dataSourceName}/schema/${schema}/tables/${name}/modify`
|
||||
);
|
||||
} else
|
||||
pushRoute(
|
||||
`data/v2/manage/table/browse?database=${dataSourceName}&table=${encodeURIComponent(
|
||||
JSON.stringify(table.table)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</CardedTable.TableBody>
|
||||
</CardedTable.Table>
|
||||
)}
|
||||
<style
|
||||
// fixes double scroll bar issue on page:
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `div[class^="RightContainer_main"] { overflow: unset; }`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -17,6 +17,7 @@ interface TableRowProps {
|
||||
reset: () => void;
|
||||
onChange: () => void;
|
||||
onTableNameClick?: () => void;
|
||||
onTableTrack?: (table: TrackableTable) => void;
|
||||
}
|
||||
|
||||
export const TableRow = React.memo(
|
||||
@ -27,6 +28,7 @@ export const TableRow = React.memo(
|
||||
reset,
|
||||
onChange,
|
||||
onTableNameClick,
|
||||
onTableTrack,
|
||||
}: TableRowProps) => {
|
||||
const [showCustomModal, setShowCustomModal] = React.useState(false);
|
||||
const { trackTables, untrackTables, isLoading } = useTrackTables({
|
||||
@ -40,7 +42,7 @@ export const TableRow = React.memo(
|
||||
}
|
||||
|
||||
trackTables({
|
||||
tablesToBeTracked: [t],
|
||||
tables: [t],
|
||||
onSuccess: () => {
|
||||
hasuraToast({
|
||||
type: 'success',
|
||||
@ -49,9 +51,9 @@ export const TableRow = React.memo(
|
||||
});
|
||||
reset();
|
||||
setShowCustomModal(false);
|
||||
onTableTrack?.(table);
|
||||
},
|
||||
onError: err => {
|
||||
console.log('!!!', err);
|
||||
hasuraToast({
|
||||
type: 'error',
|
||||
title: 'Unable to perform operation',
|
||||
@ -63,7 +65,7 @@ export const TableRow = React.memo(
|
||||
|
||||
const untrack = () => {
|
||||
untrackTables({
|
||||
tablesToBeUntracked: [table],
|
||||
tables: [table],
|
||||
onSuccess: () => {
|
||||
hasuraToast({
|
||||
type: 'success',
|
||||
@ -71,9 +73,9 @@ export const TableRow = React.memo(
|
||||
message: 'Object untracked successfully.',
|
||||
});
|
||||
reset();
|
||||
onTableTrack?.(table);
|
||||
},
|
||||
onError: err => {
|
||||
console.log('log!!');
|
||||
hasuraToast({
|
||||
type: 'error',
|
||||
title: 'Unable to perform operation',
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Table } from '../../hasura-metadata-types';
|
||||
import { TrackableTable } from './types';
|
||||
|
||||
export const getQualifiedTable = (table: Table): string[] => {
|
||||
if (Array.isArray(table)) return table;
|
||||
@ -32,10 +31,14 @@ export const paginate = <T>(
|
||||
return array.slice((page_number - 1) * page_size, page_number * page_size);
|
||||
};
|
||||
|
||||
export const search = (tables: TrackableTable[], searchText: string) => {
|
||||
if (!searchText.length) return tables;
|
||||
export const filterByText = (parentText: string, searchText: string) => {
|
||||
if (!searchText.length) return true;
|
||||
|
||||
return tables.filter(table =>
|
||||
table.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
);
|
||||
return parentText.includes(searchText.toLowerCase());
|
||||
};
|
||||
|
||||
export const filterByTableType = (type: string, selectedTypes?: string[]) => {
|
||||
if (!selectedTypes?.length) return true;
|
||||
|
||||
return selectedTypes.includes(type);
|
||||
};
|
||||
|
@ -62,7 +62,7 @@ export const TrackedFunctions = (props: TrackedFunctionsProps) => {
|
||||
|
||||
const listProps = usePaginatedSearchableList({
|
||||
data: functionsWithId,
|
||||
searchFn: searchFn,
|
||||
filterFn: searchFn,
|
||||
});
|
||||
|
||||
const {
|
||||
|
@ -86,7 +86,7 @@ export const UntrackedFunctions = (props: UntrackedFunctionsProps) => {
|
||||
|
||||
const listProps = usePaginatedSearchableList({
|
||||
data: functionsWithId,
|
||||
searchFn: (query, item) => {
|
||||
filterFn: (query, item) => {
|
||||
return item.name.toLowerCase().includes(query.toLowerCase());
|
||||
},
|
||||
});
|
||||
|
@ -11,6 +11,8 @@ export const TrackableListMenu = (
|
||||
handleTrackButton?: () => void;
|
||||
checkActionText: string;
|
||||
showButton?: boolean;
|
||||
searchChildren?: React.ReactChild;
|
||||
actionChildren?: React.ReactChild;
|
||||
}
|
||||
) => (
|
||||
<div className="flex justify-between space-x-4">
|
||||
@ -27,6 +29,7 @@ export const TrackableListMenu = (
|
||||
>
|
||||
{props.checkActionText}
|
||||
</Button>
|
||||
{props.actionChildren && props.actionChildren}
|
||||
<span className="border-r border-slate-300" />
|
||||
</>
|
||||
)}
|
||||
@ -34,6 +37,7 @@ export const TrackableListMenu = (
|
||||
{/* Search Input */}
|
||||
<div className="flex gap-2">
|
||||
<SearchBar onSearch={props.handleSearch} />
|
||||
{props.searchChildren && props.searchChildren}
|
||||
{props.searchIsActive ? (
|
||||
<Badge>{props.filteredData.length} results found</Badge>
|
||||
) : null}
|
||||
|
@ -3,9 +3,12 @@ import produce from 'immer';
|
||||
import React from 'react';
|
||||
import { AiFillCaretDown } from 'react-icons/ai';
|
||||
import { DropdownMenu } from '../../../../new-components/DropdownMenu';
|
||||
import { FaFilter } from 'react-icons/fa';
|
||||
import { BsCheck2All } from 'react-icons/bs';
|
||||
|
||||
export const useCheckRows = <T,>(
|
||||
data: (T & { id: string })[],
|
||||
filteredData: (T & { id: string })[],
|
||||
allData: (T & { id: string })[]
|
||||
) => {
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
@ -15,7 +18,8 @@ export const useCheckRows = <T,>(
|
||||
// Derived statuses
|
||||
const allChecked =
|
||||
(data.length > 0 && checkedIds.length === data.length) ||
|
||||
(allData.length > 0 && checkedIds.length === allData.length);
|
||||
(allData.length > 0 && checkedIds.length === allData.length) ||
|
||||
(filteredData.length > 0 && checkedIds.length === filteredData.length);
|
||||
|
||||
// Input field determinate status
|
||||
const partialSelection =
|
||||
@ -38,12 +42,22 @@ export const useCheckRows = <T,>(
|
||||
);
|
||||
};
|
||||
|
||||
const toggleAll = (useOriginalList?: boolean) => {
|
||||
const toggleAll = (props?: {
|
||||
useOriginalList?: boolean;
|
||||
useAllFilteredList?: boolean;
|
||||
}) => {
|
||||
const { useOriginalList, useAllFilteredList } = props ?? {};
|
||||
|
||||
if (useOriginalList) {
|
||||
setCheckedIds(allData.map(item => item.id));
|
||||
return;
|
||||
}
|
||||
|
||||
if (useAllFilteredList) {
|
||||
setCheckedIds(filteredData.map(item => item.id));
|
||||
return;
|
||||
}
|
||||
|
||||
if (allChecked) {
|
||||
setCheckedIds([]);
|
||||
} else {
|
||||
@ -75,9 +89,20 @@ export const useCheckRows = <T,>(
|
||||
<DropdownMenu
|
||||
items={[
|
||||
[
|
||||
<span className="py-1.5" onClick={() => toggleAll(true)}>
|
||||
All {allData.length} items
|
||||
</span>,
|
||||
<div
|
||||
className="py-1.5 gap-2 flex items-center"
|
||||
onClick={() => toggleAll({ useAllFilteredList: true })}
|
||||
>
|
||||
<FaFilter /> All {filteredData.length} results (filtered)
|
||||
</div>,
|
||||
],
|
||||
[
|
||||
<div
|
||||
className="py-1.5 gap-2 flex items-center"
|
||||
onClick={() => toggleAll({ useOriginalList: true })}
|
||||
>
|
||||
<BsCheck2All /> All {allData.length} items
|
||||
</div>,
|
||||
],
|
||||
]}
|
||||
>
|
||||
|
@ -5,20 +5,22 @@ import { paginate, search } from '../utils';
|
||||
|
||||
export function usePaginatedSearchableList<TData extends { id: string }>({
|
||||
data,
|
||||
searchFn,
|
||||
filterFn,
|
||||
defaultQuery,
|
||||
}: {
|
||||
data: TData[];
|
||||
searchFn: (searchText: string, item: TData) => boolean;
|
||||
filterFn: (searchText: string, item: TData) => boolean;
|
||||
defaultQuery?: string;
|
||||
}) {
|
||||
const [pageNumber, setPageNumber] = useState(DEFAULT_PAGE_NUMBER);
|
||||
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [searchText, setSearchText] = useState(defaultQuery ?? '');
|
||||
|
||||
const searchIsActive = !!searchText.length;
|
||||
|
||||
const filteredData = React.useMemo(
|
||||
() => search<TData>({ data, searchText, searchFn }),
|
||||
[data, searchFn, searchText]
|
||||
() => search<TData>({ data, searchText, filterFn }),
|
||||
[data, filterFn, searchText]
|
||||
);
|
||||
|
||||
const { data: paginatedData, totalPages } = React.useMemo(
|
||||
@ -28,7 +30,7 @@ export function usePaginatedSearchableList<TData extends { id: string }>({
|
||||
|
||||
const rowsToBeChecked = searchIsActive ? filteredData : paginatedData;
|
||||
|
||||
const checkData = useCheckRows(rowsToBeChecked, data);
|
||||
const checkData = useCheckRows(rowsToBeChecked, filteredData, data);
|
||||
|
||||
const checkedItems = React.useMemo(
|
||||
() => data.filter(d => checkData.checkedIds.includes(d.id)),
|
||||
|
@ -48,13 +48,11 @@ export const paginate = <T>({
|
||||
export function search<T>({
|
||||
data,
|
||||
searchText,
|
||||
searchFn,
|
||||
filterFn,
|
||||
}: {
|
||||
data: T[];
|
||||
searchText: string;
|
||||
searchFn: (searchText: string, item: T) => boolean;
|
||||
filterFn: (searchText: string, item: T) => boolean;
|
||||
}) {
|
||||
if (!searchText.length) return data;
|
||||
|
||||
return data.filter(item => searchFn(searchText, item));
|
||||
return data.filter(item => filterFn(searchText, item));
|
||||
}
|
||||
|
@ -32,10 +32,10 @@ export const useTrackTables = ({
|
||||
|
||||
const trackTables = useCallback(
|
||||
({
|
||||
tablesToBeTracked,
|
||||
tables,
|
||||
...mutateOptions
|
||||
}: {
|
||||
tablesToBeTracked: TrackableTable[];
|
||||
tables: TrackableTable[];
|
||||
} & MetadataMigrationOptions<BulkKeepGoingResponse>) => {
|
||||
mutate(
|
||||
{
|
||||
@ -44,7 +44,7 @@ export const useTrackTables = ({
|
||||
resource_version,
|
||||
args: {
|
||||
allow_warnings: true,
|
||||
tables: tablesToBeTracked.map(trackableTable => ({
|
||||
tables: tables.map(trackableTable => ({
|
||||
table: trackableTable.table,
|
||||
source: dataSourceName,
|
||||
configuration: trackableTable.configuration,
|
||||
@ -60,10 +60,10 @@ export const useTrackTables = ({
|
||||
|
||||
const untrackTables = useCallback(
|
||||
({
|
||||
tablesToBeUntracked,
|
||||
tables,
|
||||
...mutateOptions
|
||||
}: {
|
||||
tablesToBeUntracked: TrackableTable[];
|
||||
tables: TrackableTable[];
|
||||
} & MetadataMigrationOptions) => {
|
||||
mutate(
|
||||
{
|
||||
@ -72,7 +72,7 @@ export const useTrackTables = ({
|
||||
resource_version,
|
||||
args: {
|
||||
allow_warnings: true,
|
||||
tables: tablesToBeUntracked.map(untrackableTable => ({
|
||||
tables: tables.map(untrackableTable => ({
|
||||
table: untrackableTable.table,
|
||||
source: dataSourceName,
|
||||
configuration: untrackableTable.configuration,
|
||||
|
@ -5,6 +5,7 @@ import globals from '../../Globals';
|
||||
const relationshipTabTablesId = 'f6c57c31-abd3-46d9-aae9-b97435793273';
|
||||
const importActionFromOpenApiId = '12e5aaf4-c794-4b8f-b762-5fda0bff946a';
|
||||
const permissionsNewUI = '5f7b1673-b2ef-4c98-89f7-f30cb64f0136';
|
||||
const trackingSectionUI = 'c2536b28-0ea3-11ee-be56-0242ac120002';
|
||||
|
||||
const importActionFromOpenApi: FeatureFlagDefinition = {
|
||||
id: importActionFromOpenApiId,
|
||||
@ -21,6 +22,7 @@ export const availableFeatureFlagIds = {
|
||||
relationshipTabTablesId,
|
||||
importActionFromOpenApiId,
|
||||
permissionsNewUI,
|
||||
trackingSectionUI,
|
||||
};
|
||||
|
||||
export const availableFeatureFlags: FeatureFlagDefinition[] = [
|
||||
@ -43,6 +45,15 @@ export const availableFeatureFlags: FeatureFlagDefinition[] = [
|
||||
defaultValue: false,
|
||||
discussionUrl: '',
|
||||
},
|
||||
{
|
||||
id: trackingSectionUI,
|
||||
title: 'Enable new Table Tracking UI for Postgres & SQL Server',
|
||||
description: 'Try out the new UI experience for tracking tables',
|
||||
section: 'data',
|
||||
status: 'experimental',
|
||||
defaultValue: false,
|
||||
discussionUrl: 'https://github.com/hasura/graphql-engine/discussions/9727',
|
||||
},
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
...(isProConsole(globals) ? [importActionFromOpenApi] : []),
|
||||
];
|
||||
|
@ -0,0 +1,14 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ExperimentalFeatureBanner } from './ExperimentalFeatureBanner';
|
||||
|
||||
export default {
|
||||
title: 'features/components/Experimental Feature Banner 🧬',
|
||||
component: ExperimentalFeatureBanner,
|
||||
args: {
|
||||
githubIssueLink: 'https://github.com',
|
||||
},
|
||||
} as Meta<typeof ExperimentalFeatureBanner>;
|
||||
|
||||
export const Basic: StoryObj<typeof ExperimentalFeatureBanner> = {
|
||||
render: args => <ExperimentalFeatureBanner {...args} />,
|
||||
};
|
@ -0,0 +1,41 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { FaGithub } from 'react-icons/fa';
|
||||
import { Button } from '../../new-components/Button';
|
||||
import { IndicatorCard } from '../../new-components/IndicatorCard';
|
||||
|
||||
const twButtonExperimental = `from-purple-50 to-purple-50 border-purple-300 hover:border-purple-500 focus-visible:from-purple-200 focus-visible:to-purple-200 disabled:border-purple-300 !text-purple-800`;
|
||||
|
||||
export const ExperimentalFeatureBanner: React.VFC<{
|
||||
githubIssueLink: string;
|
||||
feedbackIcon?: JSX.Element;
|
||||
}> = ({ githubIssueLink, feedbackIcon }) => {
|
||||
return (
|
||||
<IndicatorCard
|
||||
status="experimental"
|
||||
className="py-4 px-md"
|
||||
showIcon
|
||||
contentFullWidth
|
||||
>
|
||||
<div className='flex items-center justify-between mx-4"'>
|
||||
<div>
|
||||
<h1 className="text-purple-800 font-bold text-lg">
|
||||
This is an experimental feature
|
||||
</h1>
|
||||
<div className="text-muted">
|
||||
Join the discussion on GitHub to talk about this feature or report
|
||||
bugs
|
||||
</div>
|
||||
</div>
|
||||
<a href={githubIssueLink} target="_blank" rel="noreferrer">
|
||||
<Button
|
||||
className={clsx(twButtonExperimental)}
|
||||
icon={feedbackIcon ?? <FaGithub />}
|
||||
>
|
||||
Share Feedback
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</IndicatorCard>
|
||||
);
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
# Why is this direcotory here?
|
||||
|
||||
This is a place for components that are not atomic and therefore do not belong in `new-components` but are common across many features.
|
@ -0,0 +1 @@
|
||||
export { ExperimentalFeatureBanner } from './ExperimentalFeatureBanner';
|
@ -200,13 +200,13 @@ export const CheckItem: StoryObj<typeof DropDown.Root> = {
|
||||
<DropDown.Root trigger={<Trigger />}>
|
||||
<DropDown.CheckItem
|
||||
checked={bookmarksChecked}
|
||||
onCheckChange={checked => setBookmarksChecked(checked)}
|
||||
onCheckChange={setBookmarksChecked}
|
||||
>
|
||||
Show Bookmarks
|
||||
</DropDown.CheckItem>
|
||||
<DropDown.CheckItem
|
||||
checked={urlsChecked}
|
||||
onCheckChange={checked => setUrlsChecked(checked)}
|
||||
onCheckChange={setUrlsChecked}
|
||||
>
|
||||
Show Full URLs
|
||||
</DropDown.CheckItem>
|
||||
|
@ -15,10 +15,10 @@ export const BasicItem: React.FC<{
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
disabled={disabled}
|
||||
className="group/item outline-none cursor-pointer"
|
||||
className="group/item outline-none"
|
||||
onClick={onClick}
|
||||
>
|
||||
<StyleWrappers.Item dangerous={dangerous} link={link}>
|
||||
<StyleWrappers.Item dangerous={dangerous} link={link} disabled={disabled}>
|
||||
{children}
|
||||
</StyleWrappers.Item>
|
||||
</DropdownMenu.Item>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import React from 'react';
|
||||
import { BsCheck } from 'react-icons/bs';
|
||||
import { FaCheckSquare, FaRegSquare } from 'react-icons/fa';
|
||||
import * as StyleWrappers from './style-wrappers';
|
||||
|
||||
export const CheckItem: React.FC<{
|
||||
@ -10,12 +10,19 @@ export const CheckItem: React.FC<{
|
||||
<DropdownMenu.CheckboxItem
|
||||
className="group/item outline-none"
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckChange}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
onCheckChange?.(!checked);
|
||||
}}
|
||||
>
|
||||
<StyleWrappers.Item selectable>
|
||||
<DropdownMenu.ItemIndicator className="absolute left-0 w-7 inline-flex items-center justify-center">
|
||||
<BsCheck size={15} />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
<div className="absolute left-0 w-7 inline-flex items-center justify-center">
|
||||
{!checked ? (
|
||||
<FaRegSquare className="text-blue-600" />
|
||||
) : (
|
||||
<FaCheckSquare className="text-blue-500" />
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</StyleWrappers.Item>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
|
@ -5,9 +5,10 @@ import * as StyleWrappers from './style-wrappers';
|
||||
|
||||
export const RadioItem: React.FC<{ value: string }> = ({ value, children }) => (
|
||||
<DropdownMenu.RadioItem className="group/item outline-none" value={value}>
|
||||
{console.log(value)}
|
||||
<StyleWrappers.Item selectable>
|
||||
<DropdownMenu.ItemIndicator className="absolute left-0 w-7 inline-flex items-center justify-center">
|
||||
<BsDot size={20} />
|
||||
<BsDot size={24} className="text-blue-600" />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
{children}
|
||||
</StyleWrappers.Item>
|
||||
|
@ -6,14 +6,16 @@ export const Item: React.FC<{
|
||||
dangerous?: boolean;
|
||||
link?: boolean;
|
||||
selectable?: boolean;
|
||||
}> = ({ children, dangerous, link, selectable = false }) => (
|
||||
disabled?: boolean;
|
||||
}> = ({ children, dangerous, link, selectable = false, disabled }) => (
|
||||
<div
|
||||
className={clsx(
|
||||
styles.twBaseStyle,
|
||||
selectable && styles.twSelectableItem,
|
||||
styles.twDefault,
|
||||
dangerous && styles.twDangerous,
|
||||
link && styles.twLink
|
||||
link && styles.twLink,
|
||||
disabled && 'cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
@ -9,10 +9,11 @@ export const twBaseStyle = `
|
||||
pl-3
|
||||
mx-2
|
||||
select-none
|
||||
cursor-pointer
|
||||
group-data-[disabled]/item:!pointer-events-none
|
||||
group-data-[disabled]/item:!text-neutral-200`;
|
||||
|
||||
export const twSelectableItem = `pl-6`;
|
||||
export const twSelectableItem = `pl-7`;
|
||||
|
||||
export const twDefault = `
|
||||
text-gray-900
|
||||
|
@ -7,11 +7,13 @@ import { DropdownMenu, DropdownMenuProps } from '../DropdownMenu';
|
||||
interface DropdownButtonProps extends React.ComponentProps<typeof Button> {
|
||||
items: React.ReactNode[][];
|
||||
options?: DropdownMenuProps['options'];
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export const DropdownButton: React.FC<DropdownButtonProps> = ({
|
||||
items,
|
||||
options = {},
|
||||
size,
|
||||
...rest
|
||||
}) => {
|
||||
const dropdownMenuOptions = produce(options, draft => {
|
||||
@ -30,7 +32,7 @@ export const DropdownButton: React.FC<DropdownButtonProps> = ({
|
||||
icon={
|
||||
<FaChevronDown className="transition-transform group-radix-state-open:rotate-180 w-3 h-3" />
|
||||
}
|
||||
size="sm"
|
||||
size={size ?? 'sm'}
|
||||
{...rest}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { FaCheck, FaQuestion, FaTimes } from 'react-icons/fa';
|
||||
import { FaCheck, FaFlask, FaQuestion, FaTimes } from 'react-icons/fa';
|
||||
|
||||
type indicatorCardStatus = 'info' | 'positive' | 'negative';
|
||||
type indicatorCardStatus = 'info' | 'positive' | 'negative' | 'experimental';
|
||||
|
||||
export type IndicatorCardProps = {
|
||||
status?: indicatorCardStatus;
|
||||
@ -15,10 +15,11 @@ export type IndicatorCardProps = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const cardColors: Record<indicatorCardStatus, string> = {
|
||||
const twCardColors: Record<indicatorCardStatus, string> = {
|
||||
info: 'border-l-secondary',
|
||||
negative: 'border-l-red-600',
|
||||
positive: 'border-l-green-600',
|
||||
experimental: `border-l-purple-600`,
|
||||
};
|
||||
|
||||
const IconPerStatus: Record<
|
||||
@ -28,12 +29,14 @@ const IconPerStatus: Record<
|
||||
info: FaQuestion,
|
||||
negative: FaTimes,
|
||||
positive: FaCheck,
|
||||
experimental: FaFlask,
|
||||
};
|
||||
|
||||
const iconColorsPerStatus: Record<indicatorCardStatus, string> = {
|
||||
info: 'text-blue-800 bg-indigo-100',
|
||||
positive: 'text-green-800 bg-green-100',
|
||||
negative: 'text-red-800 bg-red-100',
|
||||
experimental: 'text-purple-800 bg-purple-100',
|
||||
};
|
||||
|
||||
export const IndicatorCard = ({
|
||||
@ -52,7 +55,7 @@ export const IndicatorCard = ({
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center bg-white rounded p-md border border-gray-300 border-l-4 mb-sm',
|
||||
cardColors[status],
|
||||
twCardColors[status],
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user