Compare commits
26 Commits
split-api-
...
update-rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15b6ed2171 | ||
|
|
95c82eeb73 | ||
|
|
bb59d31d1b | ||
|
|
19b5695c9c | ||
|
|
cfac112a87 | ||
|
|
86098b292e | ||
|
|
44731d0161 | ||
|
|
817b583e45 | ||
|
|
53398e27fe | ||
|
|
4d937a858b | ||
|
|
8676fbec52 | ||
|
|
a5bf332c12 | ||
|
|
23fea78bae | ||
|
|
6d289e2a18 | ||
|
|
dfb076e6a6 | ||
|
|
affad83c0c | ||
|
|
ffd6e406ee | ||
|
|
06c00ef995 | ||
|
|
790b088abd | ||
|
|
d7c5cc6cd5 | ||
|
|
42b7d34688 | ||
|
|
6eb221ebd0 | ||
|
|
b5c1269dc8 | ||
|
|
8fe3f7e37e | ||
|
|
9cba61efce | ||
|
|
13fc7d30fb |
128
e2e/support/helpers/e2e-notebook-helpers.js
Normal file
128
e2e/support/helpers/e2e-notebook-helpers.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import { popover } from "e2e/support/helpers/e2e-ui-elements-helpers";
|
||||
|
||||
export function openNotebook() {
|
||||
return cy.icon("notebook").click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helps to select specific notebook steps like filters, joins, break outs, etc.
|
||||
*
|
||||
* Details:
|
||||
* https://github.com/metabase/metabase/pull/17708#discussion_r700082403
|
||||
*
|
||||
* @param {string} type — notebook step type (filter, join, expression, summarize, etc.)
|
||||
* @param {{stage: number; index: number}} positionConfig - indexes specifying step's position
|
||||
* @returns
|
||||
*/
|
||||
export function getNotebookStep(type, { stage = 0, index = 0 } = {}) {
|
||||
return cy.findByTestId(`step-${type}-${stage}-${index}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visualize notebook query results.
|
||||
*
|
||||
* @param {function} callback
|
||||
*/
|
||||
export function visualize(callback) {
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
|
||||
cy.button("Visualize").click();
|
||||
|
||||
cy.wait("@dataset").then(({ response }) => {
|
||||
if (callback) {
|
||||
callback(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function addSummaryField({
|
||||
metric,
|
||||
table,
|
||||
field,
|
||||
stage = 0,
|
||||
index = 0,
|
||||
}) {
|
||||
getNotebookStep("summarize", { stage, index })
|
||||
.findByTestId("aggregate-step")
|
||||
.findAllByTestId("notebook-cell-item")
|
||||
.last()
|
||||
.click();
|
||||
|
||||
popover().within(() => {
|
||||
cy.findByText(metric).click();
|
||||
if (table) {
|
||||
cy.findByText(table).click();
|
||||
}
|
||||
if (field) {
|
||||
cy.findByText(field).click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function addSummaryGroupingField({
|
||||
table,
|
||||
field,
|
||||
stage = 0,
|
||||
index = 0,
|
||||
}) {
|
||||
getNotebookStep("summarize", { stage, index })
|
||||
.findByTestId("breakout-step")
|
||||
.findAllByTestId("notebook-cell-item")
|
||||
.last()
|
||||
.click();
|
||||
|
||||
popover().within(() => {
|
||||
if (table) {
|
||||
cy.findByText(table).click();
|
||||
}
|
||||
cy.findByText(field).click();
|
||||
});
|
||||
}
|
||||
|
||||
export function removeSummaryGroupingField({ field, stage = 0, index = 0 }) {
|
||||
getNotebookStep("summarize", { stage, index })
|
||||
.findByTestId("breakout-step")
|
||||
.findByText(field)
|
||||
.icon("close")
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a raw table given a table and optional LHS and RHS column names
|
||||
* (for cases when join condition can't be selected automatically)
|
||||
*
|
||||
* Expects a join popover to be open
|
||||
*
|
||||
* @param {string} tableName
|
||||
* @param {string} [lhsColumnName]
|
||||
* @param {string} [rhsColumnName]
|
||||
*/
|
||||
export function joinTable(tableName, lhsColumnName, rhsColumnName) {
|
||||
popover().findByText(tableName).click();
|
||||
if (lhsColumnName && rhsColumnName) {
|
||||
popover().findByText(lhsColumnName).click();
|
||||
popover().findByText(rhsColumnName).click();
|
||||
}
|
||||
}
|
||||
|
||||
export function selectSavedQuestionsToJoin(
|
||||
firstQuestionName,
|
||||
secondQuestionName,
|
||||
) {
|
||||
cy.intercept("GET", "/api/database/*/schemas").as("loadSchemas");
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Saved Questions").click();
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText(firstQuestionName).click();
|
||||
cy.wait("@loadSchemas");
|
||||
|
||||
// join to question b
|
||||
cy.icon("join_left_outer").click();
|
||||
|
||||
popover().within(() => {
|
||||
cy.findByText("Sample Database").click();
|
||||
cy.findByText("Raw Data").click();
|
||||
cy.findByText("Saved Questions").click();
|
||||
cy.findByText(secondQuestionName).click();
|
||||
});
|
||||
}
|
||||
11
e2e/support/helpers/e2e-table-helpers.js
Normal file
11
e2e/support/helpers/e2e-table-helpers.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// This is needed in because table headers don't actually have an onClick handler
|
||||
// they instead have an onDragStop listener and check to see how many pixels
|
||||
// the column has moved. if it's less than 5, we treat it like a click
|
||||
export const tableHeaderClick = columnName => {
|
||||
cy.findByRole("columnheader", { name: new RegExp(columnName, "i") }).trigger(
|
||||
"mousedown",
|
||||
);
|
||||
cy.findByRole("columnheader", { name: new RegExp(columnName, "i") }).trigger(
|
||||
"mouseup",
|
||||
);
|
||||
};
|
||||
@@ -33,3 +33,4 @@ export * from "./e2e-viz-settings-helpers";
|
||||
export * from "./e2e-models-metadata-helpers";
|
||||
export * from "./e2e-ui-elements-overflow-helpers";
|
||||
export * from "./e2e-relative-date-picker-helpers";
|
||||
export * from "./e2e-table-helpers";
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
visitModel,
|
||||
visitQuestion,
|
||||
describeOSS,
|
||||
tableHeaderClick,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
const ANALYTICS_COLLECTION_NAME = "Metabase analytics";
|
||||
@@ -58,7 +59,7 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
|
||||
});
|
||||
|
||||
cy.findByTestId("TableInteractive-root").within(() => {
|
||||
cy.findByText("Last Name").click();
|
||||
tableHeaderClick("Last Name");
|
||||
});
|
||||
|
||||
popover().findByText("Filter by this column").click();
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getNotebookStep,
|
||||
checkExpressionEditorHelperPopoverPosition,
|
||||
queryBuilderMain,
|
||||
tableHeaderClick,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
const { ORDERS, ORDERS_ID, PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE;
|
||||
@@ -592,7 +593,7 @@ describe("scenarios > question > custom column", () => {
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("CustomDate").click();
|
||||
tableHeaderClick("CustomDate");
|
||||
|
||||
popover().within(() => {
|
||||
cy.findByText("Filter by this column").click();
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
visualize,
|
||||
popover,
|
||||
resetTestTable,
|
||||
tableHeaderClick,
|
||||
} from "e2e/support/helpers";
|
||||
|
||||
["postgres", "mysql"].forEach(dialect => {
|
||||
@@ -35,7 +36,7 @@ import {
|
||||
|
||||
visualize();
|
||||
|
||||
cy.findAllByTestId("header-cell").contains("Numeric").click();
|
||||
tableHeaderClick("Numeric");
|
||||
popover().findByText(/^Sum$/).click();
|
||||
|
||||
cy.wait("@dataset");
|
||||
|
||||
@@ -75,6 +75,7 @@ describe("scenarios > question > joined questions", () => {
|
||||
|
||||
// Post-join aggregation (metabase#11452):
|
||||
summarize({ mode: "notebook" });
|
||||
cy.realPress("Escape");
|
||||
addSummaryField({
|
||||
metric: "Average of ...",
|
||||
table: "Review",
|
||||
@@ -245,6 +246,7 @@ describe("scenarios > question > joined questions", () => {
|
||||
joinTable("Products");
|
||||
|
||||
summarize({ mode: "notebook" });
|
||||
cy.realPress("Escape");
|
||||
addSummaryField({ metric: "Count of rows" });
|
||||
addSummaryGroupingField({ table: "Product", field: "ID" });
|
||||
|
||||
@@ -311,6 +313,7 @@ describe("scenarios > question > joined questions", () => {
|
||||
assertJoinColumnName("right", "Created At: Day");
|
||||
|
||||
summarize({ mode: "notebook" });
|
||||
cy.realPress("Escape");
|
||||
addSummaryField({ metric: "Count of rows" });
|
||||
|
||||
visualize();
|
||||
|
||||
@@ -249,14 +249,14 @@ describe("scenarios > question > settings", () => {
|
||||
cy.findByTestId("viz-settings-button").click(); // open settings sidebar
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Conditional Formatting"); // confirm it's open
|
||||
cy.get(".TableInteractive").findByText("Subtotal").click(); // open subtotal column header actions
|
||||
cy.findByTestId("TableInteractive-root").findByText("Subtotal").click(); // open subtotal column header actions
|
||||
popover().icon("gear").click(); // open subtotal column settings
|
||||
|
||||
//cy.findByText("Table options").should("not.exist"); // no longer displaying the top level settings
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Separator style"); // shows subtotal column settings
|
||||
|
||||
cy.get(".TableInteractive").findByText("Created At").click(); // open created_at column header actions
|
||||
cy.findByTestId("TableInteractive-root").findByText("Created At").click(); // open created_at column header actions
|
||||
popover().icon("gear").click(); // open created_at column settings
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Date style"); // shows created_at column settings
|
||||
|
||||
@@ -20,7 +20,7 @@ import { getImpersonatedDatabaseId } from "metabase-enterprise/advanced_permissi
|
||||
import { ImpersonationApi } from "metabase-enterprise/services";
|
||||
import { fetchUserAttributes } from "metabase-enterprise/shared/reducer";
|
||||
import { getUserAttributes } from "metabase-enterprise/shared/selectors";
|
||||
import type { Impersonation } from "metabase-types/api";
|
||||
import type { Impersonation, UserAttribute } from "metabase-types/api";
|
||||
|
||||
import { ImpersonationModalView } from "./ImpersonationModalView";
|
||||
|
||||
@@ -87,7 +87,7 @@ const _ImpersonationModal = ({ route, params }: ImpersonationModalProps) => {
|
||||
}, [dispatch, route]);
|
||||
|
||||
const handleSave = useCallback(
|
||||
attribute => {
|
||||
(attribute: UserAttribute) => {
|
||||
dispatch(
|
||||
updateDataPermission({
|
||||
groupId,
|
||||
|
||||
@@ -53,7 +53,7 @@ const ApplicationPermissionsPage = ({
|
||||
}, [initialize]);
|
||||
|
||||
const handlePermissionChange = useCallback(
|
||||
(item, permission, value) => {
|
||||
(item: any, permission: any, value: any) => {
|
||||
updatePermission({
|
||||
groupId: item.id,
|
||||
permission,
|
||||
|
||||
@@ -67,7 +67,7 @@ export const SettingsJWTForm = ({
|
||||
}, [settings, settingValues]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
values => {
|
||||
(values: SettingValues) => {
|
||||
return onSubmit({ ...values, "jwt-enabled": true });
|
||||
},
|
||||
[onSubmit],
|
||||
|
||||
@@ -19,7 +19,7 @@ const CacheSection = ({ initialCacheTTL, onSave }: CacheSectionProps) => {
|
||||
const [cacheTTL, setCacheTTL] = useState(initialCacheTTL);
|
||||
|
||||
const handleChange = useCallback(
|
||||
number => {
|
||||
(number: number | undefined) => {
|
||||
setCacheTTL(normalizeCacheTTL(number));
|
||||
},
|
||||
[setCacheTTL],
|
||||
|
||||
@@ -24,7 +24,7 @@ interface Props {
|
||||
verifyCard: (id: number) => void;
|
||||
removeCardReview: (id: number) => void;
|
||||
isModerator: boolean;
|
||||
VerifyButton: React.FC;
|
||||
VerifyButton: React.FC<React.PropsWithChildren<unknown>>;
|
||||
verifyButtonProps: any;
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ function SnippetCollectionForm({
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async values => {
|
||||
async (values: any) => {
|
||||
const nextCollection = isEditing
|
||||
? await handleUpdate({ id: collection.id as CollectionId, ...values })
|
||||
: await handleCreate(values);
|
||||
|
||||
@@ -11,7 +11,10 @@ export function isReactDOMTypeElement(
|
||||
|
||||
export function isReactComponent(
|
||||
component: any,
|
||||
): component is React.FC | React.Component | React.ExoticComponent {
|
||||
): component is
|
||||
| React.FC<React.PropsWithChildren<unknown>>
|
||||
| React.Component
|
||||
| React.ExoticComponent {
|
||||
return (
|
||||
typeof component === "function" ||
|
||||
// Checking for "Exotic" components such as ones returned by memo, forwardRef
|
||||
|
||||
@@ -5,11 +5,11 @@ import { SmallGenericError } from "metabase/components/ErrorPages";
|
||||
|
||||
// eslint-disable-next-line import/no-default-export -- deprecated usage
|
||||
export default class ErrorBoundary extends Component<
|
||||
{
|
||||
React.PropsWithChildren<{
|
||||
onError?: (errorInfo: ErrorInfo) => void;
|
||||
errorComponent?: ComponentType;
|
||||
errorComponent?: ComponentType<React.PropsWithChildren<unknown>>;
|
||||
message?: string;
|
||||
},
|
||||
}>,
|
||||
{
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ const VerticalRadio = (props: FormRadioProps) => (
|
||||
<FormRadioWidget {...props} vertical />
|
||||
);
|
||||
|
||||
const WIDGETS: Record<InputComponentType, FunctionComponent<any>> = {
|
||||
const WIDGETS: Record<
|
||||
InputComponentType,
|
||||
FunctionComponent<React.PropsWithChildren<any>>
|
||||
> = {
|
||||
text: FormInputWidget,
|
||||
date: FormInputWidget,
|
||||
time: FormInputWidget,
|
||||
|
||||
@@ -61,7 +61,8 @@ export const ActionParameterMappingForm = ({
|
||||
}, [action]);
|
||||
|
||||
const handleParameterChange = useCallback(
|
||||
(dashboardParameterId, target) => {
|
||||
// FUTURE TODO: dashboardParameterId can be null but setParameterMapping expects ParameterId, casting to any for now
|
||||
(dashboardParameterId: any, target: ParameterTarget) => {
|
||||
dispatch(
|
||||
setParameterMapping(
|
||||
dashboardParameterId,
|
||||
|
||||
@@ -43,7 +43,10 @@ function ActionParametersInputForm({
|
||||
}, [mappedParameters, action.visualization_settings?.fields]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (parameters, actions) => {
|
||||
async (
|
||||
parameters: ParametersForActionExecution,
|
||||
actions: FormikHelpers<ParametersForActionExecution>,
|
||||
) => {
|
||||
actions.setSubmitting(true);
|
||||
const { success, error } = await onSubmit(parameters);
|
||||
if (success) {
|
||||
|
||||
@@ -169,9 +169,12 @@ function DatabaseEditApp(props: DatabaseEditAppProps) {
|
||||
return (
|
||||
<DatabaseEditRoot>
|
||||
<Breadcrumbs className="py4" crumbs={crumbs} />
|
||||
|
||||
<DatabaseEditMain>
|
||||
<ErrorBoundary errorComponent={GenericError as ComponentType}>
|
||||
<ErrorBoundary
|
||||
errorComponent={
|
||||
GenericError as ComponentType<React.PropsWithChildren<unknown>>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div className="pt0">
|
||||
<LoadingAndErrorWrapper
|
||||
@@ -209,7 +212,6 @@ function DatabaseEditApp(props: DatabaseEditAppProps) {
|
||||
/>
|
||||
)}
|
||||
</DatabaseEditMain>
|
||||
|
||||
<LeaveConfirmationModal
|
||||
isEnabled={isDirty && !isCallbackScheduled}
|
||||
route={route}
|
||||
|
||||
@@ -142,7 +142,7 @@ type Props = {
|
||||
onFilterChange: (filter: any[]) => void;
|
||||
};
|
||||
|
||||
const DatePicker: React.FC<Props> = props => {
|
||||
const DatePicker: React.FC<React.PropsWithChildren<Props>> = props => {
|
||||
const {
|
||||
className,
|
||||
filter,
|
||||
|
||||
@@ -34,7 +34,7 @@ const getIntervalString = (filter: Filter) => {
|
||||
return start.format(formatString) + " - " + end.format(formatString);
|
||||
};
|
||||
|
||||
const DatePickerFooter: React.FC<Props> = ({
|
||||
const DatePickerFooter: React.FC<React.PropsWithChildren<Props>> = ({
|
||||
filter,
|
||||
primaryColor,
|
||||
onFilterChange,
|
||||
|
||||
@@ -11,6 +11,7 @@ import _ from "underscore";
|
||||
import Grabber from "metabase/components/Grabber";
|
||||
import TippyPopoverWithTrigger from "metabase/components/PopoverWithTrigger/TippyPopoverWithTrigger";
|
||||
import AccordionList from "metabase/core/components/AccordionList";
|
||||
import type { DragEndEvent } from "metabase/core/components/Sortable";
|
||||
import { SortableList } from "metabase/core/components/Sortable";
|
||||
import Tables from "metabase/entities/tables";
|
||||
import { Icon } from "metabase/ui";
|
||||
@@ -73,8 +74,8 @@ const MetadataTableColumnList = ({
|
||||
);
|
||||
|
||||
const handleSortEnd = useCallback(
|
||||
({ itemIds: fieldOrder }) => {
|
||||
onUpdateFieldOrder(table, fieldOrder);
|
||||
({ itemIds: fieldOrder }: DragEndEvent) => {
|
||||
onUpdateFieldOrder(table, fieldOrder as number[]);
|
||||
},
|
||||
[table, onUpdateFieldOrder],
|
||||
);
|
||||
|
||||
@@ -97,7 +97,12 @@ function CollectionsPermissionsPageView({
|
||||
}, [initialize]);
|
||||
|
||||
const handlePermissionChange = useCallback(
|
||||
(item, _permission, value, toggleState) => {
|
||||
(
|
||||
item: { id: GroupId },
|
||||
_permission: unknown,
|
||||
value: unknown,
|
||||
toggleState: boolean,
|
||||
) => {
|
||||
updateCollectionPermission({
|
||||
groupId: item.id,
|
||||
collection,
|
||||
|
||||
@@ -26,7 +26,7 @@ export const CreateApiKeyModal = ({
|
||||
const [secretKey, setSecretKey] = useState<string>("");
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async vals => {
|
||||
async (vals: { name: string; group_id: string }) => {
|
||||
const response = await ApiKeysApi.create(vals);
|
||||
setSecretKey(response.unmasked_key);
|
||||
setModal("secretKey");
|
||||
|
||||
@@ -99,7 +99,7 @@ export const EditApiKeyModal = ({
|
||||
const [secretKey, setSecretKey] = useState<string>("");
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async vals => {
|
||||
async (vals: { id: number; group_id: number; name: string }) => {
|
||||
await ApiKeysApi.edit({
|
||||
id: vals.id,
|
||||
group_id: vals.group_id,
|
||||
|
||||
@@ -60,7 +60,7 @@ export const SMTPConnectionForm = ({
|
||||
}, [dispatch]);
|
||||
|
||||
const handleUpdateEmailSettings = useCallback(
|
||||
async formData => {
|
||||
async (formData: object) => {
|
||||
await dispatch(updateEmailSettings(formData));
|
||||
|
||||
if (!isEmailConfigured) {
|
||||
@@ -71,7 +71,7 @@ export const SMTPConnectionForm = ({
|
||||
);
|
||||
|
||||
const handleSendTestEmail = useCallback(
|
||||
async e => {
|
||||
async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
setSendingEmail("working");
|
||||
|
||||
@@ -87,7 +87,7 @@ export const SettingsLdapFormView = ({
|
||||
}, [settings, settingValues]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
values => {
|
||||
(values: any) => {
|
||||
return onSubmit({
|
||||
...values,
|
||||
"ldap-port": values["ldap-port"]?.trim(),
|
||||
|
||||
@@ -43,10 +43,7 @@ function AddMappingRow({
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={3} style={{ padding: 0 }}>
|
||||
<form
|
||||
className="m2 p1 bordered border-brand justify-between rounded relative flex align-center"
|
||||
onSubmit={isMappingNameUnique ? handleSubmit : undefined}
|
||||
>
|
||||
<div className="m2 p1 bordered border-brand justify-between rounded relative flex align-center">
|
||||
<input
|
||||
aria-label="new-group-mapping-name-input"
|
||||
className="input--borderless h3 ml1 flex-full"
|
||||
@@ -61,12 +58,12 @@ function AddMappingRow({
|
||||
<Button borderless onClick={handleCancelClick}>{t`Cancel`}</Button>
|
||||
<Button
|
||||
className="ml2"
|
||||
type="submit"
|
||||
primary={!!isMappingNameUnique}
|
||||
disabled={!isMappingNameUnique}
|
||||
onClick={() => (isMappingNameUnique ? handleSubmit() : undefined)}
|
||||
>{t`Add`}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -59,12 +59,14 @@ function Input({
|
||||
...props
|
||||
}: InputProps) {
|
||||
const handleChange = useCallback(
|
||||
event => onChange(event.target.value),
|
||||
(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChange(event.target.value),
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(
|
||||
event => onBlurChange(event.target.value),
|
||||
(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onBlurChange(event.target.value),
|
||||
[onBlurChange],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import { connect } from "react-redux";
|
||||
import { push, replace } from "react-router-redux";
|
||||
import { useMount } from "react-use";
|
||||
|
||||
import { getAdminPaths } from "metabase/admin/app/selectors";
|
||||
|
||||
@@ -15,11 +16,13 @@ const mapDispatchToProps = {
|
||||
};
|
||||
|
||||
const RedirectToAllowedSettings = ({ adminItems, replace }) => {
|
||||
if (adminItems.length === 0) {
|
||||
replace("/unauthorized");
|
||||
} else {
|
||||
replace(adminItems[0].path);
|
||||
}
|
||||
useMount(() => {
|
||||
if (adminItems.length === 0) {
|
||||
replace("/unauthorized");
|
||||
} else {
|
||||
replace(adminItems[0].path);
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "./SlackSetup.styled";
|
||||
|
||||
export interface SlackSetupProps {
|
||||
Form: ComponentType;
|
||||
Form: ComponentType<React.PropsWithChildren<unknown>>;
|
||||
manifest?: string;
|
||||
isBot?: boolean;
|
||||
isValid?: boolean;
|
||||
@@ -112,7 +112,7 @@ const CreateAppSection = ({ manifest }: CreateAppSectionProps): JSX.Element => {
|
||||
};
|
||||
|
||||
interface ActivateAppSectionProps {
|
||||
Form: ComponentType;
|
||||
Form: ComponentType<React.PropsWithChildren<unknown>>;
|
||||
}
|
||||
|
||||
const ActivateAppSection = ({ Form }: ActivateAppSectionProps): JSX.Element => {
|
||||
|
||||
@@ -28,11 +28,7 @@ export interface SlackStatusProps {
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const SlackStatus = ({
|
||||
Form,
|
||||
isValid,
|
||||
onDelete,
|
||||
}: SlackStatusProps): JSX.Element => {
|
||||
const SlackStatus = ({ Form, isValid, onDelete }: SlackStatusProps) => {
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface SlackStatusFormProps {
|
||||
settings: SlackSettings;
|
||||
}
|
||||
|
||||
const SlackStatusForm = ({ settings }: SlackStatusFormProps): JSX.Element => {
|
||||
const SlackStatusForm = ({ settings }: SlackStatusFormProps) => {
|
||||
return <SlackForm initialValues={settings} isReadOnly />;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
export interface FormProps {
|
||||
Form: ComponentType;
|
||||
FormField: ComponentType<FormFieldProps>;
|
||||
Form: ComponentType<React.PropsWithChildren<unknown>>;
|
||||
FormField: ComponentType<React.PropsWithChildren<FormFieldProps>>;
|
||||
}
|
||||
|
||||
export interface FormFieldProps {
|
||||
|
||||
@@ -12,12 +12,13 @@ interface SlackSetupProps {
|
||||
}
|
||||
|
||||
interface SlackSetupStateProps {
|
||||
Form: ComponentType;
|
||||
Form: ComponentType<React.PropsWithChildren<unknown>>;
|
||||
isBot?: boolean;
|
||||
isValid?: boolean;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State): SlackSetupStateProps => ({
|
||||
// @ts-expect-error - error seems to be due to old version of react-redux, upgrade should fix
|
||||
Form: SlackSetupForm,
|
||||
isBot: hasSlackBotToken(state),
|
||||
isValid: isSlackTokenValid(state),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { connect } from "react-redux";
|
||||
import { routerActions, replace } from "react-router-redux";
|
||||
import { useMount } from "react-use";
|
||||
import { connectedReduxRedirect } from "redux-auth-wrapper/history3/redirect";
|
||||
|
||||
import { getAdminPaths } from "metabase/admin/app/selectors";
|
||||
@@ -31,10 +32,13 @@ export const createAdminRedirect = (adminPath, nonAdminPath) => {
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(({ user, replace, location }) => {
|
||||
const path = `${location.pathname}/${
|
||||
user.is_superuser ? adminPath : nonAdminPath
|
||||
}`;
|
||||
replace(path);
|
||||
useMount(() => {
|
||||
const path = `${location.pathname}/${
|
||||
user.is_superuser ? adminPath : nonAdminPath
|
||||
}`;
|
||||
replace(path);
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import "ee-plugins"; // eslint-disable-line import/no-duplicates
|
||||
import { createHistory } from "history";
|
||||
import { DragDropContextProvider } from "react-dnd";
|
||||
import HTML5Backend from "react-dnd-html5-backend";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import { Router, useRouterHistory } from "react-router";
|
||||
import { syncHistoryWithStore } from "react-router-redux";
|
||||
@@ -67,14 +67,12 @@ function _init(reducers, getRoutes, callback) {
|
||||
const routes = getRoutes(store);
|
||||
const history = syncHistoryWithStore(browserHistory, store);
|
||||
|
||||
let root;
|
||||
|
||||
createTracker(store);
|
||||
|
||||
initializeEmbedding(store);
|
||||
const root = createRoot(document.getElementById("root"));
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store} ref={ref => (root = ref)}>
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<EmotionCacheProvider>
|
||||
<DragDropContextProvider backend={HTML5Backend} context={{ window }}>
|
||||
<ThemeProvider>
|
||||
@@ -84,14 +82,13 @@ function _init(reducers, getRoutes, callback) {
|
||||
</DragDropContextProvider>
|
||||
</EmotionCacheProvider>
|
||||
</Provider>,
|
||||
document.getElementById("root"),
|
||||
);
|
||||
|
||||
registerVisualizations();
|
||||
|
||||
store.dispatch(refreshSiteSettings());
|
||||
|
||||
PLUGIN_APP_INIT_FUCTIONS.forEach(init => init({ root }));
|
||||
PLUGIN_APP_INIT_FUCTIONS.forEach(init => init());
|
||||
|
||||
window.Metabase = window.Metabase || {};
|
||||
window.Metabase.store = store;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import _ from "underscore";
|
||||
|
||||
import {
|
||||
@@ -70,13 +71,11 @@ export function CollectionContent({
|
||||
const deleteBookmark = (id: BookmarkId, type: BookmarkType) =>
|
||||
dispatch(Bookmark.actions.delete({ id, type }));
|
||||
|
||||
const uploadFile = ({
|
||||
file,
|
||||
modelId,
|
||||
collectionId,
|
||||
tableId,
|
||||
}: UploadFileProps) =>
|
||||
dispatch(uploadFileAction({ file, modelId, collectionId, tableId }));
|
||||
const uploadFile = useCallback(
|
||||
({ file, modelId, collectionId, tableId }: UploadFileProps) =>
|
||||
dispatch(uploadFileAction({ file, modelId, collectionId, tableId })),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const error =
|
||||
bookmarksError || databasesError || collectionsError || collectionError;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type CollectionPickerOptions,
|
||||
CollectionPickerModal,
|
||||
} from "metabase/common/components/EntityPicker";
|
||||
import type { CollectionPickerItem } from "metabase/common/components/EntityPicker/types";
|
||||
import CollectionName from "metabase/containers/CollectionName";
|
||||
import type { FilterItemsInPersonalCollection } from "metabase/containers/ItemPicker";
|
||||
import SnippetCollectionName from "metabase/containers/SnippetCollectionName";
|
||||
@@ -31,6 +32,8 @@ export interface FormCollectionPickerProps
|
||||
onOpenCollectionChange?: (collectionId: CollectionId) => void;
|
||||
filterPersonalCollections?: FilterItemsInPersonalCollection;
|
||||
zIndex?: number;
|
||||
className?: string | undefined;
|
||||
style?: React.CSSProperties | undefined;
|
||||
}
|
||||
|
||||
function ItemName({
|
||||
@@ -88,7 +91,7 @@ function FormCollectionPicker({
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ id }) => {
|
||||
({ id }: CollectionPickerItem) => {
|
||||
setValue(canonicalCollectionId(id));
|
||||
setIsPickerOpen(false);
|
||||
},
|
||||
|
||||
@@ -47,7 +47,7 @@ export const CollectionPickerModal = ({
|
||||
}>();
|
||||
|
||||
const searchFilter = useCallback(
|
||||
searchResults =>
|
||||
(searchResults: SearchResult[]) =>
|
||||
searchResults.filter((result: SearchResult) => result.can_write),
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -58,6 +58,7 @@ export function NestedItemPicker<TItem extends TypeWithModel>({
|
||||
const { query, selectedItem } = level;
|
||||
|
||||
return (
|
||||
// @ts-expect-error -- we need to upgrade mantine to fix this type error
|
||||
<ListBox
|
||||
key={generateKey(query)}
|
||||
data-testid={`item-picker-level-${index}`}
|
||||
|
||||
@@ -70,10 +70,13 @@ function _BaseBucketPickerPopover({
|
||||
[items],
|
||||
);
|
||||
|
||||
const handleExpand = useCallback(evt => {
|
||||
evt.stopPropagation();
|
||||
setIsExpanded(true);
|
||||
}, []);
|
||||
const handleExpand = useCallback(
|
||||
(evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
evt.stopPropagation();
|
||||
setIsExpanded(true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handlePopoverClose = useCallback(() => {
|
||||
const nextState = isInitiallyExpanded(
|
||||
|
||||
@@ -170,11 +170,11 @@ export function QueryColumnPicker({
|
||||
);
|
||||
|
||||
const renderItemExtra = useCallback(
|
||||
item => (
|
||||
({ column }: { column: Lib.ColumnMetadata }) => (
|
||||
<QueryColumnInfoIcon
|
||||
query={query}
|
||||
stageIndex={stageIndex}
|
||||
column={item.column}
|
||||
column={column}
|
||||
position="right"
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -29,9 +29,13 @@ export default ComposedComponent =>
|
||||
}
|
||||
|
||||
render() {
|
||||
return ReactDOM.createPortal(
|
||||
<ComposedComponent {...this.props} className={undefined} />,
|
||||
this._element,
|
||||
return (
|
||||
<>
|
||||
{ReactDOM.createPortal(
|
||||
<ComposedComponent {...this.props} className={undefined} />,
|
||||
this._element,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -43,7 +43,9 @@ function ExplicitSize<T extends BaseInnerProps>({
|
||||
wrapped = false,
|
||||
refreshMode = "throttle",
|
||||
}: ExplicitSizeProps<T> = {}) {
|
||||
return (ComposedComponent: ComponentType<T & SizeState>) => {
|
||||
return (
|
||||
ComposedComponent: ComponentType<React.PropsWithChildren<T & SizeState>>,
|
||||
) => {
|
||||
const displayName = ComposedComponent.displayName || ComposedComponent.name;
|
||||
|
||||
class WrappedComponent extends Component<T> {
|
||||
|
||||
@@ -28,14 +28,23 @@ interface FormRenderProps<Values extends BaseFieldValues>
|
||||
FormFooter: React.ComponentType<CustomFormFooterProps>;
|
||||
}
|
||||
|
||||
type Children<Values> =
|
||||
| React.ReactNode
|
||||
| ((props: FormRenderProps<Values>) => JSX.Element);
|
||||
|
||||
export interface CustomFormProps<Values extends BaseFieldValues>
|
||||
extends BaseFormProps<Values>,
|
||||
OptionalFormViewProps {
|
||||
children?:
|
||||
| React.ReactNode
|
||||
| ((props: FormRenderProps<Values>) => JSX.Element);
|
||||
children?: Children<Values>;
|
||||
}
|
||||
|
||||
// HACK: to supress TS typings from React v18 upgrade
|
||||
const isChildrenReactNode = <Values = unknown,>(
|
||||
children: Children<Values>,
|
||||
): children is React.ReactNode => {
|
||||
return typeof children !== "function";
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@@ -43,7 +52,7 @@ function CustomForm<Values extends BaseFieldValues>(
|
||||
props: CustomFormProps<Values>,
|
||||
) {
|
||||
const { formObject: form, values, children } = props;
|
||||
if (typeof children === "function") {
|
||||
if (!isChildrenReactNode(children)) {
|
||||
return (
|
||||
<FormContext.Provider value={props}>
|
||||
{children({
|
||||
@@ -62,7 +71,7 @@ function CustomForm<Values extends BaseFieldValues>(
|
||||
|
||||
return (
|
||||
<FormContext.Provider value={props}>
|
||||
<Form {...props} />
|
||||
<Form {...props}>{children}</Form>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ function RawCustomFormField(
|
||||
});
|
||||
|
||||
const handleChange = useCallback(
|
||||
e => {
|
||||
(e: unknown) => {
|
||||
field.onChange(e);
|
||||
onChange?.(e);
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ function BaseTree({
|
||||
}, [prevData, data, selectedId, previousSelectedId, expandedIds]);
|
||||
|
||||
const handleToggleExpand = useCallback(
|
||||
itemId => {
|
||||
(itemId: ITreeNodeItem["id"]) => {
|
||||
if (expandedIds.has(itemId)) {
|
||||
setExpandedIds(prev => new Set([...prev].filter(id => id !== itemId)));
|
||||
} else {
|
||||
|
||||
@@ -20,5 +20,5 @@ export interface TreeNodeProps {
|
||||
}
|
||||
|
||||
export type TreeNodeComponent = React.ComponentType<
|
||||
TreeNodeProps & React.RefAttributes<HTMLLIElement>
|
||||
React.PropsWithChildren<TreeNodeProps & React.RefAttributes<HTMLLIElement>>
|
||||
>;
|
||||
|
||||
@@ -39,7 +39,7 @@ function Item<TId>({
|
||||
}, [item, canSelect, hasChildren, onChange, onChangeOpenCollectionId]);
|
||||
|
||||
const handleExpand = useCallback(
|
||||
event => {
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
onChangeOpenCollectionId?.(item.id);
|
||||
},
|
||||
|
||||
@@ -225,7 +225,7 @@ function ItemPicker<TId>({
|
||||
);
|
||||
|
||||
const handleCollectionOpen = useCallback(
|
||||
collectionId => {
|
||||
(collectionId: CollectionId) => {
|
||||
setOpenCollectionId(collectionId);
|
||||
onOpenCollectionChange?.(collectionId);
|
||||
},
|
||||
|
||||
@@ -39,7 +39,7 @@ interface Props<TId> {
|
||||
style?: React.CSSProperties;
|
||||
onChange: (item: PickerItem<TId>) => void;
|
||||
onSearchStringChange: (searchString: string) => void;
|
||||
onOpenCollectionChange: (collectionId: PickerItem<TId>["id"]) => void;
|
||||
onOpenCollectionChange: (collectionId: Collection["id"]) => void;
|
||||
checkCollectionMaybeHasChildren: (
|
||||
collection: CollectionPickerItem<TId>,
|
||||
) => boolean;
|
||||
@@ -74,7 +74,7 @@ function ItemPickerView<TId>({
|
||||
const canFetch = (isPickingNotCollection || searchString) && allowFetch;
|
||||
|
||||
const handleSearchInputKeyPress = useCallback(
|
||||
e => {
|
||||
(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
onSearchStringChange(e.target.value);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ export const MarkdownRoot = styled(getComponent(ReactMarkdown))<MarkdownProps>`
|
||||
}
|
||||
`;
|
||||
|
||||
function getComponent<P>(component: (props: P) => ReactElement): FC<P> {
|
||||
function getComponent<P>(
|
||||
component: (props: P) => ReactElement,
|
||||
): FC<React.PropsWithChildren<P>> {
|
||||
return component;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const SelectButton = forwardRef(function SelectButton(
|
||||
ref: Ref<HTMLButtonElement>,
|
||||
) {
|
||||
const handleClear = useCallback(
|
||||
event => {
|
||||
(event: React.MouseEvent<HTMLImageElement | SVGElement>) => {
|
||||
if (onClear) {
|
||||
// Required not to trigger the usual SelectButton's onClick handler
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -110,7 +110,7 @@ export function ClickBehaviorSidebar({
|
||||
);
|
||||
|
||||
const handleChangeSettings = useCallback(
|
||||
nextClickBehavior => {
|
||||
(nextClickBehavior?: Record<string, unknown> | null) => {
|
||||
const { id } = dashcard;
|
||||
|
||||
if (selectedColumn == null) {
|
||||
@@ -142,7 +142,7 @@ export function ClickBehaviorSidebar({
|
||||
);
|
||||
|
||||
const handleColumnSelected = useCallback(
|
||||
column => {
|
||||
(column: DatasetColumn) => {
|
||||
const originalColumnVizSettings = getClickBehaviorForColumn(
|
||||
dashcard,
|
||||
column,
|
||||
|
||||
@@ -44,9 +44,12 @@ export function CustomURLPicker({
|
||||
linkTemplate: url,
|
||||
});
|
||||
|
||||
const handleLinkTemplateChange = useCallback(e => {
|
||||
setUrl(e.currentTarget.value);
|
||||
}, []);
|
||||
const handleLinkTemplateChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUrl(e.currentTarget.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
updateSettings({
|
||||
|
||||
@@ -66,7 +66,8 @@ export function LinkOptions({
|
||||
const hasSelectedLinkType = clickBehavior.linkType != null;
|
||||
|
||||
const handleSelectLinkType = useCallback(
|
||||
type => updateSettings({ type: clickBehavior.type, linkType: type }),
|
||||
(type: CustomDestinationClickBehaviorLinkType) =>
|
||||
updateSettings({ type: clickBehavior.type, linkType: type }),
|
||||
[clickBehavior, updateSettings],
|
||||
);
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ export function LinkedEntityPicker({
|
||||
const { PickerComponent, getModalTitle } = LINK_TARGETS[linkType];
|
||||
|
||||
const handleSelectLinkTargetEntityId = useCallback(
|
||||
targetId => {
|
||||
(targetId: CardId | DashboardId) => {
|
||||
const isNewTargetEntity = targetId !== clickBehavior.targetId;
|
||||
|
||||
if (!isNewTargetEntity) {
|
||||
@@ -160,7 +160,7 @@ export function LinkedEntityPicker({
|
||||
} else {
|
||||
updateSettings({
|
||||
...clickBehavior,
|
||||
targetId,
|
||||
targetId: targetId as number,
|
||||
parameterMapping: {},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ function CloseIcon({
|
||||
}
|
||||
|
||||
interface SidebarItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
as?: React.ComponentType<any>;
|
||||
as?: React.ComponentType<React.PropsWithChildren<any>>;
|
||||
disabled?: boolean;
|
||||
padded?: boolean;
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -66,7 +66,7 @@ export function TableClickBehaviorView({
|
||||
}, [columns, getClickBehaviorForColumn]);
|
||||
|
||||
const renderColumn = useCallback(
|
||||
({ column, clickBehavior }, index) => {
|
||||
({ column, clickBehavior }: any, index: number) => {
|
||||
return (
|
||||
<Column
|
||||
key={index}
|
||||
@@ -80,7 +80,7 @@ export function TableClickBehaviorView({
|
||||
);
|
||||
|
||||
const renderColumnGroup = useCallback(
|
||||
group => {
|
||||
(group: any) => {
|
||||
const [clickBehaviorType, columnsWithClickBehavior] = group;
|
||||
return (
|
||||
<div key={clickBehaviorType} className="mb2 px4">
|
||||
|
||||
@@ -79,7 +79,7 @@ export function TypeSelector({
|
||||
}, []);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
value => {
|
||||
(value: any) => {
|
||||
if (value !== clickBehavior.type) {
|
||||
updateSettings(value === "actionMenu" ? undefined : { type: value });
|
||||
} else if (value !== "actionMenu") {
|
||||
|
||||
@@ -125,7 +125,7 @@ export function DashCardCardParameterMapper({
|
||||
const isDisabled = mappingOptions.length === 0 || isActionDashCard(dashcard);
|
||||
|
||||
const handleChangeTarget = useCallback(
|
||||
target => {
|
||||
(target: ParameterTarget | null) => {
|
||||
if (editingParameter) {
|
||||
setParameterMapping(editingParameter.id, dashcard.id, card.id, target);
|
||||
}
|
||||
|
||||
@@ -349,7 +349,7 @@ class DashboardGrid extends Component<DashboardGridProps, DashboardGridState> {
|
||||
getRowHeight() {
|
||||
const { width } = this.props;
|
||||
|
||||
const contentViewportElement = this.context;
|
||||
const contentViewportElement = this.context as any;
|
||||
const hasScroll =
|
||||
contentViewportElement?.clientHeight <
|
||||
contentViewportElement?.scrollHeight;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import styled from "@emotion/styled";
|
||||
|
||||
import { Box } from "metabase/ui";
|
||||
|
||||
export const QuestionPickerContainer = styled(Box)`
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
@@ -2,13 +2,11 @@ import { useCallback } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import type { ModalProps } from "metabase/ui";
|
||||
import { Modal } from "metabase/ui";
|
||||
import { Box, Modal } from "metabase/ui";
|
||||
import type { CardId } from "metabase-types/api";
|
||||
|
||||
import { QuestionPicker } from "../QuestionPicker";
|
||||
|
||||
import { QuestionPickerContainer } from "./QuestionPickerModal.styled";
|
||||
|
||||
interface QuestionPickerModalProps
|
||||
extends Omit<ModalProps, "title" | "onSelect"> {
|
||||
onSelect: (cardId: CardId) => void;
|
||||
@@ -36,9 +34,9 @@ export function QuestionPickerModal({
|
||||
<Modal.CloseButton />
|
||||
</Modal.Header>
|
||||
<Modal.Body p="0">
|
||||
<QuestionPickerContainer>
|
||||
<Box h="600px" style={{ overflowY: "auto" }}>
|
||||
<QuestionPicker onSelect={handleSelect} />
|
||||
</QuestionPickerContainer>
|
||||
</Box>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
|
||||
@@ -10,7 +10,9 @@ export interface EngineOption {
|
||||
|
||||
export interface EngineFieldOverride {
|
||||
name?: string;
|
||||
type?: EngineFieldType | ComponentType<EngineFieldProps>;
|
||||
type?:
|
||||
| EngineFieldType
|
||||
| ComponentType<React.PropsWithChildren<EngineFieldProps>>;
|
||||
title?: string;
|
||||
description?: ReactNode;
|
||||
placeholder?: unknown;
|
||||
|
||||
@@ -48,7 +48,9 @@ interface WrappedModalRouteProps {
|
||||
}
|
||||
|
||||
const ModalWithRoute = (
|
||||
ComposedModal: React.ComponentType<ComposedModalProps>,
|
||||
ComposedModal: React.ComponentType<
|
||||
React.PropsWithChildren<ComposedModalProps>
|
||||
>,
|
||||
modalProps = {},
|
||||
) => {
|
||||
class ModalRouteComponent extends Component<WrappedModalRouteProps> {
|
||||
@@ -77,7 +79,7 @@ const ModalWithRoute = (
|
||||
|
||||
interface ModalRouteProps {
|
||||
path: string;
|
||||
modal: React.ComponentType<ComposedModalProps>;
|
||||
modal: React.ComponentType<React.PropsWithChildren<ComposedModalProps>>;
|
||||
modalProps?: unknown;
|
||||
}
|
||||
|
||||
@@ -102,5 +104,6 @@ class _ModalRoute extends Route {
|
||||
// Casting ModalRoute as there's no way to properly type its props
|
||||
// ModalRoute extends react-router's Route which is not generic,
|
||||
// so it's impossible to extend Route's props.
|
||||
export const ModalRoute =
|
||||
_ModalRoute as unknown as React.ComponentType<ModalRouteProps>;
|
||||
export const ModalRoute = _ModalRoute as unknown as React.ComponentType<
|
||||
React.PropsWithChildren<ModalRouteProps>
|
||||
>;
|
||||
|
||||
@@ -128,7 +128,7 @@ function formatStringFallback(value: any, options: OptionsType = {}) {
|
||||
export function formatValueRaw(
|
||||
value: unknown,
|
||||
options: OptionsType = {},
|
||||
): React.ReactElement | Moment | string | number | null {
|
||||
): React.ReactElement | string | number | null {
|
||||
options = {
|
||||
jsx: false,
|
||||
remap: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useField } from "formik";
|
||||
import type { HTMLAttributes } from "react";
|
||||
import type React from "react";
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
@@ -15,6 +16,8 @@ export interface FormModelPickerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
name: string;
|
||||
title?: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
function FormModelPicker({
|
||||
@@ -43,7 +46,7 @@ function FormModelPicker({
|
||||
}, []);
|
||||
|
||||
const renderTrigger = useCallback(
|
||||
({ onClick: handleShowPopover }) => (
|
||||
({ onClick: handleShowPopover }: { onClick: () => void }) => (
|
||||
<FormField
|
||||
className={className}
|
||||
style={style}
|
||||
@@ -71,7 +74,7 @@ function FormModelPicker({
|
||||
);
|
||||
|
||||
const renderContent = useCallback(
|
||||
({ closePopover }) => {
|
||||
({ closePopover }: { closePopover: () => void }) => {
|
||||
return (
|
||||
<PopoverItemPicker
|
||||
value={{ id: value, model: "dataset" }}
|
||||
|
||||
@@ -145,7 +145,7 @@ function ModelDetailPage({
|
||||
}, [model, tab, hasActionsTab, onChangeLocation]);
|
||||
|
||||
const handleNameChange = useCallback(
|
||||
name => {
|
||||
(name?: string) => {
|
||||
if (name && name !== model.displayName()) {
|
||||
const nextCard = model.setDisplayName(name).card();
|
||||
onChangeModel(nextCard as Card);
|
||||
@@ -155,7 +155,7 @@ function ModelDetailPage({
|
||||
);
|
||||
|
||||
const handleDescriptionChange = useCallback(
|
||||
description => {
|
||||
(description?: string | null) => {
|
||||
if (model.description() !== description) {
|
||||
const nextCard = model.setDescription(description).card();
|
||||
onChangeModel(nextCard as Card);
|
||||
|
||||
@@ -81,7 +81,7 @@ function SearchBarView({ location, onSearchActive, onSearchInactive }: Props) {
|
||||
setActive();
|
||||
}, [setActive]);
|
||||
|
||||
const onTextChange = useCallback(e => {
|
||||
const onTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchText(e.target.value);
|
||||
}, []);
|
||||
|
||||
@@ -157,7 +157,7 @@ function SearchBarView({ location, onSearchActive, onSearchInactive }: Props) {
|
||||
}, [onChangeLocation, previousLocation, searchFilters, searchText]);
|
||||
|
||||
const handleInputKeyPress = useCallback(
|
||||
e => {
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && hasSearchText) {
|
||||
goToSearchApp();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { DragEndEvent } from "@dnd-kit/core";
|
||||
import { DndContext, useSensor, PointerSensor } from "@dnd-kit/core";
|
||||
import {
|
||||
restrictToVerticalAxis,
|
||||
@@ -125,7 +126,7 @@ const BookmarkList = ({
|
||||
activationConstraint: { distance: 0 },
|
||||
});
|
||||
|
||||
const onToggleBookmarks = useCallback(isVisible => {
|
||||
const onToggleBookmarks = useCallback((isVisible: boolean) => {
|
||||
localStorage.setItem("shouldDisplayBookmarks", String(isVisible));
|
||||
}, []);
|
||||
|
||||
@@ -135,10 +136,10 @@ const BookmarkList = ({
|
||||
}, []);
|
||||
|
||||
const handleSortEnd = useCallback(
|
||||
input => {
|
||||
(input: DragEndEvent) => {
|
||||
document.body.classList.remove("grabbing");
|
||||
setIsSorting(false);
|
||||
const newIndex = bookmarks.findIndex(b => b.id === input.over.id);
|
||||
const newIndex = bookmarks.findIndex(b => b.id === input.over?.id);
|
||||
const oldIndex = bookmarks.findIndex(b => b.id === input.active.id);
|
||||
reorderBookmarks({ newIndex, oldIndex });
|
||||
},
|
||||
|
||||
@@ -119,7 +119,7 @@ function MainNavbarContainer({
|
||||
}, [rootCollection, collections, currentUser]);
|
||||
|
||||
const reorderBookmarks = useCallback(
|
||||
({ newIndex, oldIndex }) => {
|
||||
({ newIndex, oldIndex }: { newIndex: number; oldIndex: number }) => {
|
||||
const newBookmarks = [...bookmarks];
|
||||
const movedBookmark = newBookmarks[oldIndex];
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ function CollectionSectionHeading({
|
||||
handleCreateNewCollection,
|
||||
}: CollectionSectionHeadingProps) {
|
||||
const renderMenu = useCallback(
|
||||
({ closePopover }) => (
|
||||
({ closePopover }: { closePopover: () => void }) => (
|
||||
<CollectionMenuList>
|
||||
<SidebarLink
|
||||
icon="add"
|
||||
|
||||
@@ -141,7 +141,10 @@ export const PLUGIN_SELECTORS = {
|
||||
getDashboardOverviewId: (_state: State) => undefined,
|
||||
};
|
||||
|
||||
export const PLUGIN_FORM_WIDGETS: Record<string, ComponentType<any>> = {};
|
||||
export const PLUGIN_FORM_WIDGETS: Record<
|
||||
string,
|
||||
ComponentType<React.PropsWithChildren<any>>
|
||||
> = {};
|
||||
|
||||
// snippet sidebar
|
||||
export const PLUGIN_SNIPPET_SIDEBAR_PLUS_MENU_OPTIONS = [];
|
||||
@@ -195,21 +198,27 @@ export const PLUGIN_COLLECTIONS = {
|
||||
};
|
||||
|
||||
export type CollectionAuthorityLevelIcon = ComponentType<
|
||||
Omit<IconProps, "name" | "tooltip"> & {
|
||||
collection: Pick<Collection, "authority_level">;
|
||||
tooltip?: "default" | "belonging";
|
||||
}
|
||||
React.PropsWithChildren<
|
||||
Omit<IconProps, "name" | "tooltip"> & {
|
||||
collection: Pick<Collection, "authority_level">;
|
||||
tooltip?: "default" | "belonging";
|
||||
}
|
||||
>
|
||||
>;
|
||||
|
||||
type CollectionInstanceAnalyticsIcon = React.ComponentType<
|
||||
Omit<IconProps, "name"> & {
|
||||
collection: Collection;
|
||||
entity: "collection" | "question" | "model" | "dashboard";
|
||||
}
|
||||
React.PropsWithChildren<
|
||||
Omit<IconProps, "name"> & {
|
||||
collection: Collection;
|
||||
entity: "collection" | "question" | "model" | "dashboard";
|
||||
}
|
||||
>
|
||||
>;
|
||||
|
||||
type FormCollectionAuthorityLevelPicker = React.ComponentType<
|
||||
HTMLAttributes<HTMLDivElement> & { name: string; title?: string }
|
||||
React.PropsWithChildren<
|
||||
HTMLAttributes<HTMLDivElement> & { name: string; title?: string }
|
||||
>
|
||||
>;
|
||||
|
||||
export const PLUGIN_COLLECTION_COMPONENTS = {
|
||||
@@ -340,7 +349,9 @@ export const PLUGIN_EMBEDDING = {
|
||||
export const PLUGIN_CONTENT_VERIFICATION = {
|
||||
VerifiedFilter: {} as SearchFilterComponent<"verified">,
|
||||
availableModelFilters: {} as AvailableModelFilters,
|
||||
ModelFilterControls: (() => null) as ComponentType<ModelFilterControlsProps>,
|
||||
ModelFilterControls: (() => null) as ComponentType<
|
||||
React.PropsWithChildren<ModelFilterControlsProps>
|
||||
>,
|
||||
sortModelsByVerification: (_a: SearchResult, _b: SearchResult) => 0,
|
||||
sortCollectionsByVerification: (
|
||||
_a: CollectionEssentials,
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { Member, User } from "metabase-types/api";
|
||||
|
||||
export interface AuthProvider {
|
||||
name: string;
|
||||
Button: ComponentType<AuthProviderButtonProps>;
|
||||
Panel?: ComponentType<AuthProviderPanelProps>;
|
||||
Button: ComponentType<React.PropsWithChildren<AuthProviderButtonProps>>;
|
||||
Panel?: ComponentType<React.PropsWithChildren<AuthProviderPanelProps>>;
|
||||
}
|
||||
|
||||
export interface AuthProviderButtonProps {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { t } from "ttag";
|
||||
import SelectList from "metabase/components/SelectList";
|
||||
import { color } from "metabase/lib/colors";
|
||||
import { space } from "metabase/styled-components/theme";
|
||||
import { Icon, Text } from "metabase/ui";
|
||||
import { Icon } from "metabase/ui";
|
||||
|
||||
export const DataSelectorSection = styled.section`
|
||||
width: 300px;
|
||||
@@ -116,11 +116,3 @@ export const TriggerContainerIcon = styled.div`
|
||||
width: 2.5rem;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const TextSchema = styled(Text)`
|
||||
font-size: 0.75em;
|
||||
color: ${color("text-light")};
|
||||
line-height: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
`;
|
||||
|
||||
@@ -2,16 +2,13 @@ import type { CSSProperties, ReactNode } from "react";
|
||||
import { t } from "ttag";
|
||||
import _ from "underscore";
|
||||
|
||||
import { color } from "metabase/lib/colors";
|
||||
import { Icon, Text } from "metabase/ui";
|
||||
import type Database from "metabase-lib/metadata/Database";
|
||||
import type Field from "metabase-lib/metadata/Field";
|
||||
import type Table from "metabase-lib/metadata/Table";
|
||||
|
||||
import {
|
||||
TextSchema,
|
||||
TriggerContainer,
|
||||
TriggerContainerIcon,
|
||||
} from "./DataSelector.styled";
|
||||
import { TriggerContainer, TriggerContainerIcon } from "./DataSelector.styled";
|
||||
|
||||
export function Trigger({
|
||||
className,
|
||||
@@ -69,10 +66,16 @@ export function FieldTrigger({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextSchema>
|
||||
<Text
|
||||
fz="0.75rem"
|
||||
c={color("text-light")}
|
||||
lh="0.75rem"
|
||||
tt="uppercase"
|
||||
lts="0.06em"
|
||||
>
|
||||
{hasMultipleSchemas && field.table.schema_name + " > "}
|
||||
{field.table.display_name}
|
||||
</TextSchema>
|
||||
</Text>
|
||||
<Text lh="1.2rem">{field.display_name}</Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ function MappedFieldPicker({
|
||||
}, []);
|
||||
|
||||
const onFieldChange = useCallback(
|
||||
fieldId => {
|
||||
(fieldId: number | null) => {
|
||||
onChange(fieldId);
|
||||
selectButtonRef.current?.focus();
|
||||
},
|
||||
|
||||
@@ -22,7 +22,9 @@ export type StepUIItem = {
|
||||
transparent?: boolean;
|
||||
compact?: boolean;
|
||||
getColor: () => string;
|
||||
component: React.ComponentType<NotebookStepUiComponentProps>;
|
||||
component: React.ComponentType<
|
||||
React.PropsWithChildren<NotebookStepUiComponentProps>
|
||||
>;
|
||||
};
|
||||
|
||||
export const STEP_UI: Record<string, StepUIItem> = {
|
||||
|
||||
@@ -127,7 +127,7 @@ function SnippetForm({
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async values => {
|
||||
async (values: any) => {
|
||||
if (isEditing) {
|
||||
await handleUpdate({ ...values, id: snippet.id });
|
||||
} else {
|
||||
|
||||
@@ -95,7 +95,7 @@ const ChartTypeSidebar = ({
|
||||
);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(display, e) => {
|
||||
(display: string, e: React.MouseEvent) => {
|
||||
if (display === question.display()) {
|
||||
openChartSettings(e);
|
||||
} else {
|
||||
|
||||
@@ -104,10 +104,6 @@ export const XRayButton = styled(Button)<
|
||||
height: 2rem;
|
||||
`;
|
||||
|
||||
export const DescriptionSection = styled(Box)`
|
||||
margin-top: 0.5rem;
|
||||
`;
|
||||
|
||||
export const DescriptionDivider = styled(Divider)`
|
||||
border-radius: ${({ theme }) => theme.radius.xs};
|
||||
`;
|
||||
|
||||
@@ -7,13 +7,12 @@ import { useDispatch } from "metabase/lib/redux";
|
||||
import { isSyncCompleted } from "metabase/lib/syncing";
|
||||
import { PLUGIN_MODERATION } from "metabase/plugins";
|
||||
import type { WrappedResult } from "metabase/search/types";
|
||||
import { Group, Loader, Icon } from "metabase/ui";
|
||||
import { Group, Loader, Icon, Box } from "metabase/ui";
|
||||
|
||||
import { InfoText } from "../InfoText";
|
||||
|
||||
import {
|
||||
DescriptionDivider,
|
||||
DescriptionSection,
|
||||
LoadingSection,
|
||||
ResultNameSection,
|
||||
ResultTitle,
|
||||
@@ -119,7 +118,7 @@ export function SearchResult({
|
||||
</Group>
|
||||
<InfoText showLinks={!onClick} result={result} isCompact={compact} />
|
||||
{description && showDescription && (
|
||||
<DescriptionSection>
|
||||
<Box mt="0.5rem">
|
||||
<Group noWrap spacing="sm" data-testid="result-description">
|
||||
<DescriptionDivider
|
||||
size="md"
|
||||
@@ -135,7 +134,7 @@ export function SearchResult({
|
||||
{description}
|
||||
</SearchResultDescription>
|
||||
</Group>
|
||||
</DescriptionSection>
|
||||
</Box>
|
||||
)}
|
||||
</ResultNameSection>
|
||||
{isLoading && (
|
||||
|
||||
@@ -76,8 +76,12 @@ interface SearchFilter<T extends FilterTypeKeys = any> {
|
||||
export interface SearchFilterDropdown<T extends FilterTypeKeys = any>
|
||||
extends SearchFilter {
|
||||
type: "dropdown";
|
||||
DisplayComponent: ComponentType<Pick<SearchFilterComponentProps<T>, "value">>;
|
||||
ContentComponent: ComponentType<SearchFilterComponentProps<T>>;
|
||||
DisplayComponent: ComponentType<
|
||||
React.PropsWithChildren<Pick<SearchFilterComponentProps<T>, "value">>
|
||||
>;
|
||||
ContentComponent: ComponentType<
|
||||
React.PropsWithChildren<SearchFilterComponentProps<T>>
|
||||
>;
|
||||
}
|
||||
|
||||
export interface SearchFilterToggle extends SearchFilter {
|
||||
|
||||
@@ -9,7 +9,9 @@ type MenuItemProps = MantineMenuItemProps &
|
||||
// check useClickOutside hook in mantine
|
||||
export function MenuItem(props: MenuItemProps) {
|
||||
const handleMouseDownCapture = (event: MouseEvent) => {
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
const target = event.target as HTMLElement;
|
||||
target.setAttribute("data-ignore-outside-clicks", "true");
|
||||
// event.nativeEvent.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
return <Menu.Item {...props} onMouseDownCapture={handleMouseDownCapture} />;
|
||||
|
||||
@@ -20,9 +20,10 @@ export const getPopoverOverrides = (): MantineThemeOverride["components"] => ({
|
||||
PopoverDropdown: {
|
||||
defaultProps: {
|
||||
onMouseDownCapture: (event: SyntheticEvent) => {
|
||||
// prevent nested popovers from closing each other
|
||||
// see useClickOutside in @mantine/hooks for the reference
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
// // HACK: prevent nested popovers from closing each other
|
||||
// // see useClickOutside in @mantine/hooks for the reference
|
||||
const target = event.target as HTMLElement;
|
||||
target.setAttribute("data-ignore-outside-clicks", "true");
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ type Props = {
|
||||
hint?: string;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
widget?: React.ComponentType<{ id: string }>;
|
||||
widget?: React.ComponentType<React.PropsWithChildren<{ id: string }>>;
|
||||
inline?: boolean;
|
||||
marginBottom?: string;
|
||||
props?: Record<string, unknown>;
|
||||
|
||||
@@ -18,12 +18,9 @@ interface Props {
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export const DeleteObjectModal: FunctionComponent<Props> = ({
|
||||
actionId,
|
||||
objectId,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}) => {
|
||||
export const DeleteObjectModal: FunctionComponent<
|
||||
React.PropsWithChildren<Props>
|
||||
> = ({ actionId, objectId, onClose, onSuccess }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
|
||||
@@ -87,4 +87,6 @@ type OwnProps = Omit<
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(ObjectDetailWrapper) as unknown as React.ComponentType<OwnProps>;
|
||||
)(ObjectDetailWrapper) as unknown as React.ComponentType<
|
||||
React.PropsWithChildren<OwnProps>
|
||||
>;
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface ObjectDetailHeaderProps {
|
||||
}[];
|
||||
canZoom: boolean;
|
||||
objectName: string;
|
||||
objectId: ObjectId | null | unknown;
|
||||
objectId: ObjectId | null;
|
||||
canZoomPreviousRow: boolean;
|
||||
canZoomNextRow?: boolean;
|
||||
showControls?: boolean;
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import cx from "classnames";
|
||||
import PropTypes from "prop-types";
|
||||
import { createRef, forwardRef, Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { findDOMNode } from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { connect } from "react-redux";
|
||||
import { Grid, ScrollSync } from "react-virtualized";
|
||||
import { t } from "ttag";
|
||||
@@ -257,10 +258,14 @@ class TableInteractive extends Component {
|
||||
const {
|
||||
data: { cols, rows },
|
||||
} = this.props;
|
||||
const root = createRoot(this._div);
|
||||
|
||||
ReactDOM.render(
|
||||
root.render(
|
||||
<EmotionCacheProvider>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div
|
||||
style={{ display: "flex" }}
|
||||
ref={() => this.onMeasureHeaderRender(root)}
|
||||
>
|
||||
{cols.map((column, columnIndex) => (
|
||||
<div className="fake-column" key={"column-" + columnIndex}>
|
||||
{this.tableHeaderRenderer({
|
||||
@@ -282,43 +287,44 @@ class TableInteractive extends Component {
|
||||
))}
|
||||
</div>
|
||||
</EmotionCacheProvider>,
|
||||
this._div,
|
||||
() => {
|
||||
const contentWidths = [].map.call(
|
||||
this._div.getElementsByClassName("fake-column"),
|
||||
columnElement => columnElement.offsetWidth,
|
||||
);
|
||||
|
||||
const columnWidths = cols.map((col, index) => {
|
||||
if (this.columnNeedsResize) {
|
||||
if (
|
||||
this.columnNeedsResize[index] &&
|
||||
!this.columnHasResized[index]
|
||||
) {
|
||||
this.columnHasResized[index] = true;
|
||||
return contentWidths[index] + 1; // + 1 to make sure it doen't wrap?
|
||||
} else if (this.state.columnWidths[index]) {
|
||||
return this.state.columnWidths[index];
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return contentWidths[index] + 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Doing this on next tick makes sure it actually gets removed on initial measure
|
||||
setTimeout(() => {
|
||||
ReactDOM.unmountComponentAtNode(this._div);
|
||||
}, 0);
|
||||
|
||||
delete this.columnNeedsResize;
|
||||
|
||||
this.setState({ contentWidths, columnWidths }, this.recomputeGridSize);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
onMeasureHeaderRender = root => {
|
||||
const {
|
||||
data: { cols },
|
||||
} = this.props;
|
||||
|
||||
const contentWidths = [].map.call(
|
||||
this._div.getElementsByClassName("fake-column"),
|
||||
columnElement => columnElement.offsetWidth,
|
||||
);
|
||||
|
||||
const columnWidths = cols.map((col, index) => {
|
||||
if (this.columnNeedsResize) {
|
||||
if (this.columnNeedsResize[index] && !this.columnHasResized[index]) {
|
||||
this.columnHasResized[index] = true;
|
||||
return contentWidths[index] + 1; // + 1 to make sure it doen't wrap?
|
||||
} else if (this.state.columnWidths[index]) {
|
||||
return this.state.columnWidths[index];
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return contentWidths[index] + 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Doing this on next tick makes sure it actually gets removed on initial measure
|
||||
setTimeout(() => {
|
||||
root.unmount();
|
||||
}, 0);
|
||||
|
||||
delete this.columnNeedsResize;
|
||||
|
||||
this.setState({ contentWidths, columnWidths }, this.recomputeGridSize);
|
||||
};
|
||||
|
||||
recomputeGridSize = () => {
|
||||
if (this.header && this.grid) {
|
||||
this.header.recomputeGridSize();
|
||||
@@ -917,7 +923,7 @@ class TableInteractive extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollOffset = ReactDOM.findDOMNode(this.grid)?.scrollTop || 0;
|
||||
const scrollOffset = findDOMNode(this.grid)?.scrollTop || 0;
|
||||
|
||||
// infer row index from mouse position when we hover the gutter column
|
||||
if (event?.currentTarget?.id === "gutter-column") {
|
||||
@@ -1055,7 +1061,7 @@ class TableInteractive extends Component {
|
||||
columnWidth={this.getDisplayColumnWidth}
|
||||
cellRenderer={props =>
|
||||
gutterColumn && props.columnIndex === 0
|
||||
? () => null // we need a phantom cell to properly offset columns
|
||||
? null // we need a phantom cell to properly offset columns
|
||||
: this.tableHeaderRenderer({
|
||||
...props,
|
||||
columnIndex: props.columnIndex - gutterColumn,
|
||||
@@ -1084,7 +1090,7 @@ class TableInteractive extends Component {
|
||||
rowHeight={ROW_HEIGHT}
|
||||
cellRenderer={props =>
|
||||
gutterColumn && props.columnIndex === 0
|
||||
? () => null // we need a phantom cell to properly offset columns
|
||||
? null // we need a phantom cell to properly offset columns
|
||||
: this.cellRenderer({
|
||||
...props,
|
||||
columnIndex: props.columnIndex - gutterColumn,
|
||||
@@ -1108,7 +1114,7 @@ class TableInteractive extends Component {
|
||||
}
|
||||
|
||||
_benchmark() {
|
||||
const grid = ReactDOM.findDOMNode(this.grid);
|
||||
const grid = findDOMNode(this.grid);
|
||||
const height = grid.scrollHeight;
|
||||
let top = 0;
|
||||
let start = Date.now();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import cx from "classnames";
|
||||
import type React from "react";
|
||||
import { useCallback, useMemo, isValidElement } from "react";
|
||||
|
||||
import ExternalLink from "metabase/core/components/ExternalLink";
|
||||
@@ -149,7 +150,7 @@ export function TableCell({
|
||||
const isClickable = !isLink;
|
||||
|
||||
const onClick = useCallback(
|
||||
e => {
|
||||
(e: React.MouseEvent<HTMLSpanElement>) => {
|
||||
if (checkIsVisualizationClickable(clicked)) {
|
||||
onVisualizationClick?.({
|
||||
...clicked,
|
||||
|
||||
@@ -79,7 +79,7 @@ function TableSimpleInner({
|
||||
}: TableSimpleProps) {
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(1);
|
||||
const [sortColumn, setSortColumn] = useState(null);
|
||||
const [sortColumn, setSortColumn] = useState<number | null>(null);
|
||||
const [sortDirection, setSortDirection] = useState("asc");
|
||||
|
||||
const headerRef = useRef(null);
|
||||
@@ -100,7 +100,7 @@ function TableSimpleInner({
|
||||
}, [height, pageSize]);
|
||||
|
||||
const setSort = useCallback(
|
||||
colIndex => {
|
||||
(colIndex: number) => {
|
||||
if (sortColumn === colIndex) {
|
||||
setSortDirection(direction => (direction === "asc" ? "desc" : "asc"));
|
||||
} else {
|
||||
@@ -111,7 +111,7 @@ function TableSimpleInner({
|
||||
);
|
||||
|
||||
const checkIsVisualizationClickable = useCallback(
|
||||
clickedItem => {
|
||||
(clickedItem: ClickObject) => {
|
||||
return Boolean(
|
||||
onVisualizationClick &&
|
||||
visualizationIsClickable &&
|
||||
@@ -160,7 +160,7 @@ function TableSimpleInner({
|
||||
);
|
||||
|
||||
const renderColumnHeader = useCallback(
|
||||
(col, colIndex) => {
|
||||
(col: DatasetColumn, colIndex: number) => {
|
||||
const iconName = sortDirection === "desc" ? "chevrondown" : "chevronup";
|
||||
const onClick = () => setSort(colIndex);
|
||||
return (
|
||||
@@ -180,7 +180,7 @@ function TableSimpleInner({
|
||||
);
|
||||
|
||||
const renderRow = useCallback(
|
||||
(rowIndex, index) => {
|
||||
(rowIndex: number, index: number) => {
|
||||
const ref = index === 0 ? firstRowRef : null;
|
||||
return (
|
||||
<tr key={rowIndex} ref={ref} data-testid="table-row">
|
||||
|
||||
@@ -83,7 +83,7 @@ export const ChartSettingOrderedSimple = ({
|
||||
[onChangeSeriesColor],
|
||||
);
|
||||
|
||||
const getId = useCallback(item => item.key, []);
|
||||
const getId = useCallback((item: { key: string | number }) => item.key, []);
|
||||
|
||||
return (
|
||||
<ChartSettingOrderedSimpleRoot>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import styled from "@emotion/styled";
|
||||
|
||||
import Select from "metabase/core/components/Select";
|
||||
import { color } from "metabase/lib/colors";
|
||||
|
||||
export const GroupName = styled.p`
|
||||
font-weight: 700;
|
||||
color: ${color("text-medium")};
|
||||
`;
|
||||
|
||||
export const StyledSelect = styled(Select)``;
|
||||
|
||||
export const ColumnItemContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
margin-top: 1rem;
|
||||
|
||||
${StyledSelect} {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
@@ -1,150 +0,0 @@
|
||||
import type { ChangeEvent } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { t } from "ttag";
|
||||
import _ from "underscore";
|
||||
|
||||
import Button from "metabase/core/components/Button";
|
||||
import type Question from "metabase-lib/Question";
|
||||
import { getColumnKey } from "metabase-lib/queries/utils/get-column-key";
|
||||
import type {
|
||||
ConcreteFieldReference,
|
||||
DatasetColumn,
|
||||
FieldId,
|
||||
} from "metabase-types/api";
|
||||
|
||||
import {
|
||||
ColumnItemContainer,
|
||||
GroupName,
|
||||
StyledSelect,
|
||||
} from "./ChartSettingsListColumns.styled";
|
||||
|
||||
type FieldIdOrFieldRef = FieldId | ConcreteFieldReference;
|
||||
|
||||
type SettingValue = {
|
||||
left: FieldIdOrFieldRef[];
|
||||
right: FieldIdOrFieldRef[];
|
||||
};
|
||||
|
||||
interface Props {
|
||||
value: SettingValue;
|
||||
columns: DatasetColumn[];
|
||||
question: Question;
|
||||
onChange: (value: SettingValue) => void;
|
||||
onShowWidget: (config: unknown, targetElement: HTMLElement | null) => void;
|
||||
}
|
||||
|
||||
type ListColumnSlot = "left" | "right";
|
||||
|
||||
function formatValueForSelect(value: FieldIdOrFieldRef): string | number {
|
||||
const isFieldReference = Array.isArray(value);
|
||||
return isFieldReference ? JSON.stringify(value) : value;
|
||||
}
|
||||
|
||||
function parseSelectValue(
|
||||
event: ChangeEvent<HTMLSelectElement>,
|
||||
): FieldIdOrFieldRef {
|
||||
const eventValue = event.target.value;
|
||||
const isFieldReference = typeof eventValue === "string";
|
||||
return isFieldReference ? JSON.parse(eventValue) : eventValue;
|
||||
}
|
||||
|
||||
function ChartSettingsListColumns({
|
||||
value,
|
||||
columns,
|
||||
onChange,
|
||||
onShowWidget,
|
||||
}: Props) {
|
||||
const onChangeColumn = useCallback(
|
||||
(slot: ListColumnSlot, index: number, val: FieldIdOrFieldRef) => {
|
||||
onChange({
|
||||
...value,
|
||||
[slot]: [
|
||||
...value[slot].slice(0, index),
|
||||
val,
|
||||
...value[slot].slice(index + 1),
|
||||
],
|
||||
});
|
||||
},
|
||||
[value, onChange],
|
||||
);
|
||||
|
||||
const onColumnSettingsClick = useCallback(
|
||||
(fieldIdOrFieldRef, targetElement) => {
|
||||
const column = columns.find(
|
||||
column =>
|
||||
column.id === fieldIdOrFieldRef ||
|
||||
_.isEqual(column.field_ref, fieldIdOrFieldRef),
|
||||
);
|
||||
if (column) {
|
||||
onShowWidget(
|
||||
{
|
||||
id: "column_settings",
|
||||
props: {
|
||||
initialKey: getColumnKey(column),
|
||||
},
|
||||
},
|
||||
targetElement,
|
||||
);
|
||||
}
|
||||
},
|
||||
[columns, onShowWidget],
|
||||
);
|
||||
|
||||
const columnOptions = columns.map(column => ({
|
||||
name: column.display_name,
|
||||
value: column.id || JSON.stringify(column.field_ref),
|
||||
}));
|
||||
|
||||
const options = [
|
||||
{
|
||||
name: t`None`,
|
||||
value: null,
|
||||
},
|
||||
...columnOptions,
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GroupName>{t`Left`}</GroupName>
|
||||
{value.left.map((fieldIdOrFieldRef, index) => (
|
||||
<ColumnItemContainer key={index}>
|
||||
<StyledSelect
|
||||
value={formatValueForSelect(fieldIdOrFieldRef)}
|
||||
options={options}
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
onChangeColumn("left", index, parseSelectValue(e));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon="gear"
|
||||
onlyIcon
|
||||
disabled={fieldIdOrFieldRef === null}
|
||||
onClick={e => onColumnSettingsClick(fieldIdOrFieldRef, e.target)}
|
||||
/>
|
||||
</ColumnItemContainer>
|
||||
))}
|
||||
<GroupName>{t`Right`}</GroupName>
|
||||
{value.right.map((fieldIdOrFieldRef, index) => (
|
||||
<ColumnItemContainer key={index}>
|
||||
<StyledSelect
|
||||
key={index}
|
||||
value={formatValueForSelect(fieldIdOrFieldRef)}
|
||||
options={options}
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
onChangeColumn("right", index, parseSelectValue(e));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon="gear"
|
||||
onlyIcon
|
||||
disabled={fieldIdOrFieldRef === null}
|
||||
onClick={e => onColumnSettingsClick(fieldIdOrFieldRef, e.target)}
|
||||
/>
|
||||
</ColumnItemContainer>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export -- deprecated usage
|
||||
export default ChartSettingsListColumns;
|
||||
@@ -1,2 +0,0 @@
|
||||
// eslint-disable-next-line import/no-default-export -- deprecated usage
|
||||
export { default } from "./ChartSettingsListColumns";
|
||||
@@ -94,7 +94,7 @@ export interface VisualizationProps {
|
||||
export type ColumnSettingDefinition<TValue, TProps = unknown> = {
|
||||
title?: string;
|
||||
hint?: string;
|
||||
widget?: string | React.ComponentType<any>;
|
||||
widget?: string | React.ComponentType<React.PropsWithChildren<any>>;
|
||||
default?: TValue;
|
||||
props?: TProps;
|
||||
inline?: boolean;
|
||||
@@ -113,7 +113,7 @@ export type VisualizationSettingDefinition<TValue, TProps = void> = {
|
||||
section?: string;
|
||||
title?: string;
|
||||
group?: string;
|
||||
widget?: string | React.ComponentType<TProps>;
|
||||
widget?: string | React.ComponentType<React.PropsWithChildren<TProps>>;
|
||||
isValid?: (series: Series, settings: VisualizationSettings) => boolean;
|
||||
getHidden?: (series: Series, settings: VisualizationSettings) => boolean;
|
||||
getDefault?: (series: Series, settings: VisualizationSettings) => TValue;
|
||||
@@ -149,7 +149,9 @@ export type VisualizationGridSize = {
|
||||
};
|
||||
|
||||
// TODO: add component property for the react component instead of the intersection
|
||||
export type Visualization = React.ComponentType<VisualizationProps> & {
|
||||
export type Visualization = React.ComponentType<
|
||||
React.PropsWithChildren<VisualizationProps>
|
||||
> & {
|
||||
name: string;
|
||||
noun: string;
|
||||
uiName: string;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user