Compare commits
9 Commits
python-lib
...
python-lib
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd50ffa7dd | ||
|
|
2880033f06 | ||
|
|
87161c6d71 | ||
|
|
89117dbfb0 | ||
|
|
3a1b93b79d | ||
|
|
2f651e074d | ||
|
|
54a2642138 | ||
|
|
d3a74d538d | ||
|
|
82b097feb8 |
@@ -0,0 +1,35 @@
|
||||
import { Api } from "metabase/api";
|
||||
|
||||
export type PythonLibrary = {
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type GetPythonLibraryRequest = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type UpdatePythonLibraryRequest = {
|
||||
name: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export const pythonLibraryApi = Api.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
getPythonLibrary: builder.query<PythonLibrary, GetPythonLibraryRequest>({
|
||||
query: ({ name }) => ({
|
||||
url: `/api/ee/python-transform/user-modules/${name}`,
|
||||
method: "GET",
|
||||
}),
|
||||
}),
|
||||
updatePythonLibrary: builder.mutation<void, UpdatePythonLibraryRequest>({
|
||||
query: ({ name, source }) => ({
|
||||
url: `/api/ee/python-transform/user-modules/${name}`,
|
||||
method: "PUT",
|
||||
body: { source },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetPythonLibraryQuery, useUpdatePythonLibraryMutation } =
|
||||
pythonLibraryApi;
|
||||
@@ -0,0 +1,4 @@
|
||||
.editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import cx from "classnames";
|
||||
|
||||
import { CodeEditor } from "metabase/common/components/CodeEditor";
|
||||
|
||||
import S from "./PythonEditor.module.css";
|
||||
import { completion } from "./utils";
|
||||
|
||||
export function PythonEditor({
|
||||
value,
|
||||
onChange,
|
||||
withPandasCompletions,
|
||||
className,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
withPandasCompletions?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<CodeEditor
|
||||
className={cx(S.editor, className)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
language="python"
|
||||
extensions={withPandasCompletions ? completion : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./PythonEditor";
|
||||
@@ -0,0 +1,675 @@
|
||||
import {
|
||||
type Completion,
|
||||
type CompletionContext,
|
||||
autocompletion,
|
||||
} from "@codemirror/autocomplete";
|
||||
import { pythonLanguage } from "@codemirror/lang-python";
|
||||
|
||||
// This is the closest you can get to "loading pandas definitions"
|
||||
|
||||
const pandasDefinitions: Record<string, Completion[]> = {
|
||||
pd: [
|
||||
{
|
||||
label: "DataFrame",
|
||||
type: "class",
|
||||
info: "Two-dimensional, size-mutable, potentially heterogeneous tabular data",
|
||||
},
|
||||
{
|
||||
label: "Series",
|
||||
type: "class",
|
||||
info: "One-dimensional ndarray with axis labels",
|
||||
},
|
||||
{
|
||||
label: "read_csv",
|
||||
type: "function",
|
||||
info: "Read a comma-separated values (csv) file into DataFrame",
|
||||
},
|
||||
{
|
||||
label: "read_excel",
|
||||
type: "function",
|
||||
info: "Read an Excel file into a pandas DataFrame",
|
||||
},
|
||||
{
|
||||
label: "read_json",
|
||||
type: "function",
|
||||
info: "Convert a JSON string to pandas object",
|
||||
},
|
||||
{
|
||||
label: "read_sql",
|
||||
type: "function",
|
||||
info: "Read SQL query or database table into a DataFrame",
|
||||
},
|
||||
{
|
||||
label: "read_parquet",
|
||||
type: "function",
|
||||
info: "Load a parquet object from file path",
|
||||
},
|
||||
{
|
||||
label: "concat",
|
||||
type: "function",
|
||||
info: "Concatenate pandas objects along a particular axis",
|
||||
},
|
||||
{
|
||||
label: "merge",
|
||||
type: "function",
|
||||
info: "Merge DataFrame or named Series objects with a database-style join",
|
||||
},
|
||||
{
|
||||
label: "pivot_table",
|
||||
type: "function",
|
||||
info: "Create a spreadsheet-style pivot table as a DataFrame",
|
||||
},
|
||||
{
|
||||
label: "crosstab",
|
||||
type: "function",
|
||||
info: "Compute a simple cross tabulation of two (or more) factors",
|
||||
},
|
||||
{
|
||||
label: "get_dummies",
|
||||
type: "function",
|
||||
info: "Convert categorical variable(s) into dummy/indicator variables",
|
||||
},
|
||||
{
|
||||
label: "date_range",
|
||||
type: "function",
|
||||
info: "Return a fixed frequency DatetimeIndex",
|
||||
},
|
||||
{
|
||||
label: "to_datetime",
|
||||
type: "function",
|
||||
info: "Convert argument to datetime",
|
||||
},
|
||||
{
|
||||
label: "to_numeric",
|
||||
type: "function",
|
||||
info: "Convert argument to a numeric type",
|
||||
},
|
||||
{
|
||||
label: "cut",
|
||||
type: "function",
|
||||
info: "Bin values into discrete intervals",
|
||||
},
|
||||
{
|
||||
label: "qcut",
|
||||
type: "function",
|
||||
info: "Quantile-based discretization function",
|
||||
},
|
||||
{
|
||||
label: "isna",
|
||||
type: "function",
|
||||
info: "Detect missing values for an array-like object",
|
||||
},
|
||||
{
|
||||
label: "isnull",
|
||||
type: "function",
|
||||
info: "Detect missing values for an array-like object",
|
||||
},
|
||||
{
|
||||
label: "notna",
|
||||
type: "function",
|
||||
info: "Detect non-missing values for an array-like object",
|
||||
},
|
||||
{
|
||||
label: "notnull",
|
||||
type: "function",
|
||||
info: "Detect non-missing values for an array-like object",
|
||||
},
|
||||
],
|
||||
|
||||
// DataFrame methods and properties
|
||||
DataFrame: [
|
||||
// Viewing/Inspection
|
||||
{ label: "head", type: "method", info: "Return the first n rows" },
|
||||
{ label: "tail", type: "method", info: "Return the last n rows" },
|
||||
{
|
||||
label: "info",
|
||||
type: "method",
|
||||
info: "Print a concise summary of a DataFrame",
|
||||
},
|
||||
{
|
||||
label: "describe",
|
||||
type: "method",
|
||||
info: "Generate descriptive statistics",
|
||||
},
|
||||
{
|
||||
label: "shape",
|
||||
type: "property",
|
||||
info: "Return a tuple representing the dimensionality",
|
||||
},
|
||||
{
|
||||
label: "size",
|
||||
type: "property",
|
||||
info: "Return an int representing the number of elements",
|
||||
},
|
||||
{
|
||||
label: "columns",
|
||||
type: "property",
|
||||
info: "The column labels of the DataFrame",
|
||||
},
|
||||
{
|
||||
label: "index",
|
||||
type: "property",
|
||||
info: "The index (row labels) of the DataFrame",
|
||||
},
|
||||
{
|
||||
label: "dtypes",
|
||||
type: "property",
|
||||
info: "Return the dtypes in the DataFrame",
|
||||
},
|
||||
{
|
||||
label: "values",
|
||||
type: "property",
|
||||
info: "Return a Numpy representation of the DataFrame",
|
||||
},
|
||||
|
||||
// Selection/Indexing
|
||||
{
|
||||
label: "loc",
|
||||
type: "property",
|
||||
info: "Access a group of rows and columns by label(s)",
|
||||
},
|
||||
{
|
||||
label: "iloc",
|
||||
type: "property",
|
||||
info: "Purely integer-location based indexing",
|
||||
},
|
||||
{
|
||||
label: "at",
|
||||
type: "property",
|
||||
info: "Access a single value for a row/column label pair",
|
||||
},
|
||||
{
|
||||
label: "iat",
|
||||
type: "property",
|
||||
info: "Access a single value for a row/column pair by integer position",
|
||||
},
|
||||
{
|
||||
label: "query",
|
||||
type: "method",
|
||||
info: "Query the columns of a DataFrame with a boolean expression",
|
||||
},
|
||||
{
|
||||
label: "filter",
|
||||
type: "method",
|
||||
info: "Subset the dataframe rows or columns according to labels",
|
||||
},
|
||||
{
|
||||
label: "where",
|
||||
type: "method",
|
||||
info: "Replace values where the condition is False",
|
||||
},
|
||||
{
|
||||
label: "mask",
|
||||
type: "method",
|
||||
info: "Replace values where the condition is True",
|
||||
},
|
||||
|
||||
// Data Cleaning
|
||||
{
|
||||
label: "drop",
|
||||
type: "method",
|
||||
info: "Drop specified labels from rows or columns",
|
||||
},
|
||||
{
|
||||
label: "drop_duplicates",
|
||||
type: "method",
|
||||
info: "Return DataFrame with duplicate rows removed",
|
||||
},
|
||||
{ label: "dropna", type: "method", info: "Remove missing values" },
|
||||
{
|
||||
label: "fillna",
|
||||
type: "method",
|
||||
info: "Fill NA/NaN values using the specified method",
|
||||
},
|
||||
{
|
||||
label: "replace",
|
||||
type: "method",
|
||||
info: "Replace values given in to_replace with value",
|
||||
},
|
||||
{
|
||||
label: "interpolate",
|
||||
type: "method",
|
||||
info: "Fill NaN values using an interpolation method",
|
||||
},
|
||||
{
|
||||
label: "duplicated",
|
||||
type: "method",
|
||||
info: "Return boolean Series denoting duplicate rows",
|
||||
},
|
||||
{ label: "isnull", type: "method", info: "Detect missing values" },
|
||||
{ label: "isna", type: "method", info: "Detect missing values" },
|
||||
{
|
||||
label: "notnull",
|
||||
type: "method",
|
||||
info: "Detect existing (non-missing) values",
|
||||
},
|
||||
{
|
||||
label: "notna",
|
||||
type: "method",
|
||||
info: "Detect existing (non-missing) values",
|
||||
},
|
||||
|
||||
// Transformations
|
||||
{
|
||||
label: "sort_values",
|
||||
type: "method",
|
||||
info: "Sort by the values along either axis",
|
||||
},
|
||||
{
|
||||
label: "sort_index",
|
||||
type: "method",
|
||||
info: "Sort object by labels (along an axis)",
|
||||
},
|
||||
{
|
||||
label: "reset_index",
|
||||
type: "method",
|
||||
info: "Reset the index, or a level of it",
|
||||
},
|
||||
{
|
||||
label: "set_index",
|
||||
type: "method",
|
||||
info: "Set the DataFrame index using existing columns",
|
||||
},
|
||||
{
|
||||
label: "reindex",
|
||||
type: "method",
|
||||
info: "Conform Series/DataFrame to new index",
|
||||
},
|
||||
{ label: "rename", type: "method", info: "Alter axes labels" },
|
||||
{
|
||||
label: "assign",
|
||||
type: "method",
|
||||
info: "Assign new columns to a DataFrame",
|
||||
},
|
||||
{
|
||||
label: "pipe",
|
||||
type: "method",
|
||||
info: "Apply chainable functions that expect Series or DataFrames",
|
||||
},
|
||||
|
||||
// Aggregation/GroupBy
|
||||
{
|
||||
label: "groupby",
|
||||
type: "method",
|
||||
info: "Group DataFrame using a mapper or by a Series of columns",
|
||||
},
|
||||
{
|
||||
label: "agg",
|
||||
type: "method",
|
||||
info: "Aggregate using one or more operations",
|
||||
},
|
||||
{
|
||||
label: "aggregate",
|
||||
type: "method",
|
||||
info: "Aggregate using one or more operations",
|
||||
},
|
||||
{
|
||||
label: "apply",
|
||||
type: "method",
|
||||
info: "Apply a function along an axis of the DataFrame",
|
||||
},
|
||||
{
|
||||
label: "applymap",
|
||||
type: "method",
|
||||
info: "Apply a function to a Dataframe elementwise",
|
||||
},
|
||||
{
|
||||
label: "transform",
|
||||
type: "method",
|
||||
info: "Call func on self producing a DataFrame with transformed values",
|
||||
},
|
||||
{
|
||||
label: "rolling",
|
||||
type: "method",
|
||||
info: "Provide rolling window calculations",
|
||||
},
|
||||
{
|
||||
label: "expanding",
|
||||
type: "method",
|
||||
info: "Provide expanding transformations",
|
||||
},
|
||||
{ label: "resample", type: "method", info: "Resample time-series data" },
|
||||
|
||||
// Statistical Methods
|
||||
{
|
||||
label: "sum",
|
||||
type: "method",
|
||||
info: "Return the sum of the values for the requested axis",
|
||||
},
|
||||
{
|
||||
label: "mean",
|
||||
type: "method",
|
||||
info: "Return the mean of the values for the requested axis",
|
||||
},
|
||||
{
|
||||
label: "median",
|
||||
type: "method",
|
||||
info: "Return the median of the values for the requested axis",
|
||||
},
|
||||
{
|
||||
label: "mode",
|
||||
type: "method",
|
||||
info: "Get the mode(s) of each element along the selected axis",
|
||||
},
|
||||
{
|
||||
label: "std",
|
||||
type: "method",
|
||||
info: "Return sample standard deviation over requested axis",
|
||||
},
|
||||
{
|
||||
label: "var",
|
||||
type: "method",
|
||||
info: "Return unbiased variance over requested axis",
|
||||
},
|
||||
{
|
||||
label: "min",
|
||||
type: "method",
|
||||
info: "Return the minimum of the values for the requested axis",
|
||||
},
|
||||
{
|
||||
label: "max",
|
||||
type: "method",
|
||||
info: "Return the maximum of the values for the requested axis",
|
||||
},
|
||||
{
|
||||
label: "count",
|
||||
type: "method",
|
||||
info: "Count non-NA cells for each column or row",
|
||||
},
|
||||
{
|
||||
label: "nunique",
|
||||
type: "method",
|
||||
info: "Count number of distinct elements in specified axis",
|
||||
},
|
||||
{
|
||||
label: "value_counts",
|
||||
type: "method",
|
||||
info: "Return a Series containing counts of unique values",
|
||||
},
|
||||
{
|
||||
label: "corr",
|
||||
type: "method",
|
||||
info: "Compute pairwise correlation of columns",
|
||||
},
|
||||
{
|
||||
label: "cov",
|
||||
type: "method",
|
||||
info: "Compute pairwise covariance of columns",
|
||||
},
|
||||
{
|
||||
label: "rank",
|
||||
type: "method",
|
||||
info: "Compute numerical data ranks (1 through n) along axis",
|
||||
},
|
||||
{
|
||||
label: "quantile",
|
||||
type: "method",
|
||||
info: "Return values at the given quantile over requested axis",
|
||||
},
|
||||
{
|
||||
label: "clip",
|
||||
type: "method",
|
||||
info: "Trim values at input threshold(s)",
|
||||
},
|
||||
|
||||
// Merging/Joining
|
||||
{
|
||||
label: "merge",
|
||||
type: "method",
|
||||
info: "Merge DataFrame or named Series objects",
|
||||
},
|
||||
{
|
||||
label: "join",
|
||||
type: "method",
|
||||
info: "Join columns of another DataFrame",
|
||||
},
|
||||
{
|
||||
label: "concat",
|
||||
type: "method",
|
||||
info: "Concatenate pandas objects along a particular axis",
|
||||
},
|
||||
{
|
||||
label: "append",
|
||||
type: "method",
|
||||
info: "Append rows of other to the end of caller",
|
||||
},
|
||||
{
|
||||
label: "update",
|
||||
type: "method",
|
||||
info: "Modify in place using non-NA values from another DataFrame",
|
||||
},
|
||||
|
||||
// Reshaping
|
||||
{
|
||||
label: "pivot",
|
||||
type: "method",
|
||||
info: "Return reshaped DataFrame organized by given index / column values",
|
||||
},
|
||||
{
|
||||
label: "pivot_table",
|
||||
type: "method",
|
||||
info: "Create a spreadsheet-style pivot table",
|
||||
},
|
||||
{
|
||||
label: "melt",
|
||||
type: "method",
|
||||
info: "Unpivot a DataFrame from wide to long format",
|
||||
},
|
||||
{
|
||||
label: "stack",
|
||||
type: "method",
|
||||
info: "Stack the prescribed level(s) from columns to index",
|
||||
},
|
||||
{
|
||||
label: "unstack",
|
||||
type: "method",
|
||||
info: "Pivot a level of the (necessarily hierarchical) index labels",
|
||||
},
|
||||
{ label: "transpose", type: "method", info: "Transpose index and columns" },
|
||||
{ label: "T", type: "property", info: "Transpose index and columns" },
|
||||
|
||||
// I/O
|
||||
{
|
||||
label: "to_csv",
|
||||
type: "method",
|
||||
info: "Write object to a comma-separated values (csv) file",
|
||||
},
|
||||
{
|
||||
label: "to_excel",
|
||||
type: "method",
|
||||
info: "Write object to an Excel sheet",
|
||||
},
|
||||
{
|
||||
label: "to_json",
|
||||
type: "method",
|
||||
info: "Convert the object to a JSON string",
|
||||
},
|
||||
{
|
||||
label: "to_sql",
|
||||
type: "method",
|
||||
info: "Write records stored in a DataFrame to a SQL database",
|
||||
},
|
||||
{
|
||||
label: "to_parquet",
|
||||
type: "method",
|
||||
info: "Write a DataFrame to the binary parquet format",
|
||||
},
|
||||
{
|
||||
label: "to_dict",
|
||||
type: "method",
|
||||
info: "Convert the DataFrame to a dictionary",
|
||||
},
|
||||
{
|
||||
label: "to_html",
|
||||
type: "method",
|
||||
info: "Render a DataFrame as an HTML table",
|
||||
},
|
||||
{
|
||||
label: "to_string",
|
||||
type: "method",
|
||||
info: "Render a DataFrame to a console-friendly tabular output",
|
||||
},
|
||||
{
|
||||
label: "to_numpy",
|
||||
type: "method",
|
||||
info: "Convert the DataFrame to a NumPy array",
|
||||
},
|
||||
|
||||
// Time Series
|
||||
{
|
||||
label: "shift",
|
||||
type: "method",
|
||||
info: "Shift index by desired number of periods",
|
||||
},
|
||||
{ label: "tshift", type: "method", info: "Shift the time index" },
|
||||
{
|
||||
label: "diff",
|
||||
type: "method",
|
||||
info: "First discrete difference of element",
|
||||
},
|
||||
{
|
||||
label: "pct_change",
|
||||
type: "method",
|
||||
info: "Percentage change between the current and a prior element",
|
||||
},
|
||||
|
||||
// Plotting (if matplotlib available)
|
||||
{
|
||||
label: "plot",
|
||||
type: "method",
|
||||
info: "Make plots of Series or DataFrame",
|
||||
},
|
||||
{
|
||||
label: "hist",
|
||||
type: "method",
|
||||
info: "Make a histogram of the DataFrame's columns",
|
||||
},
|
||||
{
|
||||
label: "boxplot",
|
||||
type: "method",
|
||||
info: "Make a box plot of the DataFrame columns",
|
||||
},
|
||||
],
|
||||
|
||||
// Series methods (subset of DataFrame methods that make sense for Series)
|
||||
Series: [
|
||||
{ label: "head", type: "method", info: "Return the first n values" },
|
||||
{ label: "tail", type: "method", info: "Return the last n values" },
|
||||
{
|
||||
label: "describe",
|
||||
type: "method",
|
||||
info: "Generate descriptive statistics",
|
||||
},
|
||||
{
|
||||
label: "value_counts",
|
||||
type: "method",
|
||||
info: "Return a Series containing counts of unique values",
|
||||
},
|
||||
{
|
||||
label: "unique",
|
||||
type: "method",
|
||||
info: "Return unique values of Series object",
|
||||
},
|
||||
{
|
||||
label: "nunique",
|
||||
type: "method",
|
||||
info: "Return number of unique elements in the object",
|
||||
},
|
||||
{ label: "sort_values", type: "method", info: "Sort by the values" },
|
||||
{
|
||||
label: "sort_index",
|
||||
type: "method",
|
||||
info: "Sort Series by index labels",
|
||||
},
|
||||
{
|
||||
label: "reset_index",
|
||||
type: "method",
|
||||
info: "Generate a new DataFrame or Series with the index reset",
|
||||
},
|
||||
{
|
||||
label: "dropna",
|
||||
type: "method",
|
||||
info: "Return a new Series with missing values removed",
|
||||
},
|
||||
{
|
||||
label: "fillna",
|
||||
type: "method",
|
||||
info: "Fill NA/NaN values using the specified method",
|
||||
},
|
||||
{
|
||||
label: "apply",
|
||||
type: "method",
|
||||
info: "Invoke function on values of Series",
|
||||
},
|
||||
{
|
||||
label: "map",
|
||||
type: "method",
|
||||
info: "Map values of Series according to input correspondence",
|
||||
},
|
||||
{
|
||||
label: "replace",
|
||||
type: "method",
|
||||
info: "Replace values given in to_replace with value",
|
||||
},
|
||||
{
|
||||
label: "str",
|
||||
type: "property",
|
||||
info: "Vectorized string functions for Series and Index",
|
||||
},
|
||||
{
|
||||
label: "dt",
|
||||
type: "property",
|
||||
info: "Accessor object for datetimelike properties",
|
||||
},
|
||||
{
|
||||
label: "cat",
|
||||
type: "property",
|
||||
info: "Accessor object for categorical properties",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function genericPandasCompletions(context: CompletionContext) {
|
||||
const word = context.matchBefore(/[\w.]*/);
|
||||
if (!word || (word.from === word.to && !context.explicit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = word.text;
|
||||
|
||||
// Match patterns like "pd.", "df.", etc.
|
||||
if (text.includes(".")) {
|
||||
const parts = text.split(".");
|
||||
const obj = parts.at(-2);
|
||||
const partial = parts.at(-1) ?? "";
|
||||
|
||||
if (!obj || !pandasDefinitions[obj]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = pandasDefinitions[obj]
|
||||
.filter(
|
||||
(item) =>
|
||||
!partial ||
|
||||
item.label.toLowerCase().startsWith(partial.toLowerCase()),
|
||||
)
|
||||
.map((comp) => ({
|
||||
...comp,
|
||||
info: undefined,
|
||||
boost: comp.type === "method" ? 2 : comp.type === "function" ? 1 : 0,
|
||||
}));
|
||||
|
||||
return {
|
||||
filter: false,
|
||||
from: word.from + text.length - partial.length,
|
||||
options,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const completion = [
|
||||
pythonLanguage.data.of({
|
||||
autocomplete: genericPandasCompletions,
|
||||
}),
|
||||
autocompletion(),
|
||||
];
|
||||
@@ -60,3 +60,9 @@
|
||||
right: 0;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.libraryActions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@@ -3,18 +3,27 @@ import { ResizableBox } from "react-resizable";
|
||||
import { c, t } from "ttag";
|
||||
|
||||
import EmptyCodeResult from "assets/img/empty-states/code.svg";
|
||||
import { CodeEditor } from "metabase/common/components/CodeEditor";
|
||||
import DebouncedFrame from "metabase/common/components/DebouncedFrame";
|
||||
import Link from "metabase/common/components/Link";
|
||||
import { LoadingSpinner } from "metabase/common/components/MetadataInfo/MetadataInfo.styled";
|
||||
import { isMac } from "metabase/lib/browser";
|
||||
import RunButtonWithTooltip from "metabase/query_builder/components/RunButtonWithTooltip";
|
||||
import { Box, Flex, Icon, Stack, Text, Title } from "metabase/ui";
|
||||
import { Box, Checkbox, Flex, Icon, Stack, Text, Title } from "metabase/ui";
|
||||
import { getPythonLibraryUrl } from "metabase-enterprise/transforms/urls";
|
||||
|
||||
import { SHARED_LIB_IMPORT_NAME } from "../../../constants";
|
||||
import { PythonEditor as PythonCodeEditor } from "../../PythonEditor";
|
||||
import { ResizableBoxHandle } from "../EditorBody/ResizableBoxHandle";
|
||||
|
||||
import { ExecutionOutputTable } from "./ExecutionOutputTable";
|
||||
import S from "./PythonEditor.module.css";
|
||||
import { type ExecutionResult, completion, useTestPythonScript } from "./utils";
|
||||
import {
|
||||
type ExecutionResult,
|
||||
hasImport,
|
||||
insertImport,
|
||||
removeImport,
|
||||
useTestPythonScript,
|
||||
} from "./utils";
|
||||
|
||||
type PythonEditorProps = {
|
||||
script: string;
|
||||
@@ -36,6 +45,15 @@ export function PythonEditor({
|
||||
const { isRunning, isDirty, cancel, run, executionResult } =
|
||||
useTestPythonScript(script, tables);
|
||||
|
||||
const hasSharedLib = hasImport(script, SHARED_LIB_IMPORT_NAME);
|
||||
function handleToggleSharedLib() {
|
||||
if (hasImport(script, SHARED_LIB_IMPORT_NAME)) {
|
||||
onChange(removeImport(script, SHARED_LIB_IMPORT_NAME));
|
||||
} else {
|
||||
onChange(insertImport(script, SHARED_LIB_IMPORT_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack h="100%" w="100%" gap={0}>
|
||||
<ResizableBox
|
||||
@@ -46,12 +64,10 @@ export function PythonEditor({
|
||||
resizeHandles={["s"]}
|
||||
>
|
||||
<Flex h="100%" align="end" bg="bg-light">
|
||||
<CodeEditor
|
||||
className={S.editor}
|
||||
<PythonCodeEditor
|
||||
value={script}
|
||||
onChange={onChange}
|
||||
language="python"
|
||||
extensions={completion}
|
||||
withPandasCompletions
|
||||
/>
|
||||
|
||||
<Box p="md">
|
||||
@@ -65,6 +81,23 @@ export function PythonEditor({
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Stack className={S.libraryActions} p="md" gap="sm">
|
||||
<Checkbox
|
||||
label={t`Import shared library`}
|
||||
checked={hasSharedLib}
|
||||
onChange={handleToggleSharedLib}
|
||||
size="sm"
|
||||
/>
|
||||
<Flex
|
||||
component={Link}
|
||||
target="_blank"
|
||||
to={getPythonLibraryUrl({ name: SHARED_LIB_IMPORT_NAME })}
|
||||
gap="sm"
|
||||
>
|
||||
<Icon name="pencil" />
|
||||
{t`Edit shared library`}
|
||||
</Flex>
|
||||
</Stack>
|
||||
</ResizableBox>
|
||||
|
||||
<DebouncedFrame className={S.visualization}>
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import {
|
||||
type Completion,
|
||||
type CompletionContext,
|
||||
autocompletion,
|
||||
} from "@codemirror/autocomplete";
|
||||
import { pythonLanguage } from "@codemirror/lang-python";
|
||||
import { useRef, useState } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
@@ -106,672 +100,32 @@ export function useTestPythonScript(
|
||||
};
|
||||
}
|
||||
|
||||
// Static Pandas Definitions for CodeMirror Autocomplete
|
||||
// This is the closest you can get to "loading pandas definitions"
|
||||
export function insertImport(source: string, lib: string) {
|
||||
const lines = source.split("\n");
|
||||
|
||||
const pandasDefinitions: Record<string, Completion[]> = {
|
||||
pd: [
|
||||
{
|
||||
label: "DataFrame",
|
||||
type: "class",
|
||||
info: "Two-dimensional, size-mutable, potentially heterogeneous tabular data",
|
||||
},
|
||||
{
|
||||
label: "Series",
|
||||
type: "class",
|
||||
info: "One-dimensional ndarray with axis labels",
|
||||
},
|
||||
{
|
||||
label: "read_csv",
|
||||
type: "function",
|
||||
info: "Read a comma-separated values (csv) file into DataFrame",
|
||||
},
|
||||
{
|
||||
label: "read_excel",
|
||||
type: "function",
|
||||
info: "Read an Excel file into a pandas DataFrame",
|
||||
},
|
||||
{
|
||||
label: "read_json",
|
||||
type: "function",
|
||||
info: "Convert a JSON string to pandas object",
|
||||
},
|
||||
{
|
||||
label: "read_sql",
|
||||
type: "function",
|
||||
info: "Read SQL query or database table into a DataFrame",
|
||||
},
|
||||
{
|
||||
label: "read_parquet",
|
||||
type: "function",
|
||||
info: "Load a parquet object from file path",
|
||||
},
|
||||
{
|
||||
label: "concat",
|
||||
type: "function",
|
||||
info: "Concatenate pandas objects along a particular axis",
|
||||
},
|
||||
{
|
||||
label: "merge",
|
||||
type: "function",
|
||||
info: "Merge DataFrame or named Series objects with a database-style join",
|
||||
},
|
||||
{
|
||||
label: "pivot_table",
|
||||
type: "function",
|
||||
info: "Create a spreadsheet-style pivot table as a DataFrame",
|
||||
},
|
||||
{
|
||||
label: "crosstab",
|
||||
type: "function",
|
||||
info: "Compute a simple cross tabulation of two (or more) factors",
|
||||
},
|
||||
{
|
||||
label: "get_dummies",
|
||||
type: "function",
|
||||
info: "Convert categorical variable(s) into dummy/indicator variables",
|
||||
},
|
||||
{
|
||||
label: "date_range",
|
||||
type: "function",
|
||||
info: "Return a fixed frequency DatetimeIndex",
|
||||
},
|
||||
{
|
||||
label: "to_datetime",
|
||||
type: "function",
|
||||
info: "Convert argument to datetime",
|
||||
},
|
||||
{
|
||||
label: "to_numeric",
|
||||
type: "function",
|
||||
info: "Convert argument to a numeric type",
|
||||
},
|
||||
{
|
||||
label: "cut",
|
||||
type: "function",
|
||||
info: "Bin values into discrete intervals",
|
||||
},
|
||||
{
|
||||
label: "qcut",
|
||||
type: "function",
|
||||
info: "Quantile-based discretization function",
|
||||
},
|
||||
{
|
||||
label: "isna",
|
||||
type: "function",
|
||||
info: "Detect missing values for an array-like object",
|
||||
},
|
||||
{
|
||||
label: "isnull",
|
||||
type: "function",
|
||||
info: "Detect missing values for an array-like object",
|
||||
},
|
||||
{
|
||||
label: "notna",
|
||||
type: "function",
|
||||
info: "Detect non-missing values for an array-like object",
|
||||
},
|
||||
{
|
||||
label: "notnull",
|
||||
type: "function",
|
||||
info: "Detect non-missing values for an array-like object",
|
||||
},
|
||||
],
|
||||
|
||||
// DataFrame methods and properties
|
||||
DataFrame: [
|
||||
// Viewing/Inspection
|
||||
{ label: "head", type: "method", info: "Return the first n rows" },
|
||||
{ label: "tail", type: "method", info: "Return the last n rows" },
|
||||
{
|
||||
label: "info",
|
||||
type: "method",
|
||||
info: "Print a concise summary of a DataFrame",
|
||||
},
|
||||
{
|
||||
label: "describe",
|
||||
type: "method",
|
||||
info: "Generate descriptive statistics",
|
||||
},
|
||||
{
|
||||
label: "shape",
|
||||
type: "property",
|
||||
info: "Return a tuple representing the dimensionality",
|
||||
},
|
||||
{
|
||||
label: "size",
|
||||
type: "property",
|
||||
info: "Return an int representing the number of elements",
|
||||
},
|
||||
{
|
||||
label: "columns",
|
||||
type: "property",
|
||||
info: "The column labels of the DataFrame",
|
||||
},
|
||||
{
|
||||
label: "index",
|
||||
type: "property",
|
||||
info: "The index (row labels) of the DataFrame",
|
||||
},
|
||||
{
|
||||
label: "dtypes",
|
||||
type: "property",
|
||||
info: "Return the dtypes in the DataFrame",
|
||||
},
|
||||
{
|
||||
label: "values",
|
||||
type: "property",
|
||||
info: "Return a Numpy representation of the DataFrame",
|
||||
},
|
||||
|
||||
// Selection/Indexing
|
||||
{
|
||||
label: "loc",
|
||||
type: "property",
|
||||
info: "Access a group of rows and columns by label(s)",
|
||||
},
|
||||
{
|
||||
label: "iloc",
|
||||
type: "property",
|
||||
info: "Purely integer-location based indexing",
|
||||
},
|
||||
{
|
||||
label: "at",
|
||||
type: "property",
|
||||
info: "Access a single value for a row/column label pair",
|
||||
},
|
||||
{
|
||||
label: "iat",
|
||||
type: "property",
|
||||
info: "Access a single value for a row/column pair by integer position",
|
||||
},
|
||||
{
|
||||
label: "query",
|
||||
type: "method",
|
||||
info: "Query the columns of a DataFrame with a boolean expression",
|
||||
},
|
||||
{
|
||||
label: "filter",
|
||||
type: "method",
|
||||
info: "Subset the dataframe rows or columns according to labels",
|
||||
},
|
||||
{
|
||||
label: "where",
|
||||
type: "method",
|
||||
info: "Replace values where the condition is False",
|
||||
},
|
||||
{
|
||||
label: "mask",
|
||||
type: "method",
|
||||
info: "Replace values where the condition is True",
|
||||
},
|
||||
|
||||
// Data Cleaning
|
||||
{
|
||||
label: "drop",
|
||||
type: "method",
|
||||
info: "Drop specified labels from rows or columns",
|
||||
},
|
||||
{
|
||||
label: "drop_duplicates",
|
||||
type: "method",
|
||||
info: "Return DataFrame with duplicate rows removed",
|
||||
},
|
||||
{ label: "dropna", type: "method", info: "Remove missing values" },
|
||||
{
|
||||
label: "fillna",
|
||||
type: "method",
|
||||
info: "Fill NA/NaN values using the specified method",
|
||||
},
|
||||
{
|
||||
label: "replace",
|
||||
type: "method",
|
||||
info: "Replace values given in to_replace with value",
|
||||
},
|
||||
{
|
||||
label: "interpolate",
|
||||
type: "method",
|
||||
info: "Fill NaN values using an interpolation method",
|
||||
},
|
||||
{
|
||||
label: "duplicated",
|
||||
type: "method",
|
||||
info: "Return boolean Series denoting duplicate rows",
|
||||
},
|
||||
{ label: "isnull", type: "method", info: "Detect missing values" },
|
||||
{ label: "isna", type: "method", info: "Detect missing values" },
|
||||
{
|
||||
label: "notnull",
|
||||
type: "method",
|
||||
info: "Detect existing (non-missing) values",
|
||||
},
|
||||
{
|
||||
label: "notna",
|
||||
type: "method",
|
||||
info: "Detect existing (non-missing) values",
|
||||
},
|
||||
|
||||
// Transformations
|
||||
{
|
||||
label: "sort_values",
|
||||
type: "method",
|
||||
info: "Sort by the values along either axis",
|
||||
},
|
||||
{
|
||||
label: "sort_index",
|
||||
type: "method",
|
||||
info: "Sort object by labels (along an axis)",
|
||||
},
|
||||
{
|
||||
label: "reset_index",
|
||||
type: "method",
|
||||
info: "Reset the index, or a level of it",
|
||||
},
|
||||
{
|
||||
label: "set_index",
|
||||
type: "method",
|
||||
info: "Set the DataFrame index using existing columns",
|
||||
},
|
||||
{
|
||||
label: "reindex",
|
||||
type: "method",
|
||||
info: "Conform Series/DataFrame to new index",
|
||||
},
|
||||
{ label: "rename", type: "method", info: "Alter axes labels" },
|
||||
{
|
||||
label: "assign",
|
||||
type: "method",
|
||||
info: "Assign new columns to a DataFrame",
|
||||
},
|
||||
{
|
||||
label: "pipe",
|
||||
type: "method",
|
||||
info: "Apply chainable functions that expect Series or DataFrames",
|
||||
},
|
||||
|
||||
// Aggregation/GroupBy
|
||||
{
|
||||
label: "groupby",
|
||||
type: "method",
|
||||
info: "Group DataFrame using a mapper or by a Series of columns",
|
||||
},
|
||||
{
|
||||
label: "agg",
|
||||
type: "method",
|
||||
info: "Aggregate using one or more operations",
|
||||
},
|
||||
{
|
||||
label: "aggregate",
|
||||
type: "method",
|
||||
info: "Aggregate using one or more operations",
|
||||
},
|
||||
{
|
||||
label: "apply",
|
||||
type: "method",
|
||||
info: "Apply a function along an axis of the DataFrame",
|
||||
},
|
||||
{
|
||||
label: "applymap",
|
||||
type: "method",
|
||||
info: "Apply a function to a Dataframe elementwise",
|
||||
},
|
||||
{
|
||||
label: "transform",
|
||||
type: "method",
|
||||
info: "Call func on self producing a DataFrame with transformed values",
|
||||
},
|
||||
{
|
||||
label: "rolling",
|
||||
type: "method",
|
||||
info: "Provide rolling window calculations",
|
||||
},
|
||||
{
|
||||
label: "expanding",
|
||||
type: "method",
|
||||
info: "Provide expanding transformations",
|
||||
},
|
||||
{ label: "resample", type: "method", info: "Resample time-series data" },
|
||||
|
||||
// Statistical Methods
|
||||
{
|
||||
label: "sum",
|
||||
type: "method",
|
||||
info: "Return the sum of the values for the requested axis",
|
||||
},
|
||||
{
|
||||
label: "mean",
|
||||
type: "method",
|
||||
info: "Return the mean of the values for the requested axis",
|
||||
},
|
||||
{
|
||||
label: "median",
|
||||
type: "method",
|
||||
info: "Return the median of the values for the requested axis",
|
||||
},
|
||||
{
|
||||
label: "mode",
|
||||
type: "method",
|
||||
info: "Get the mode(s) of each element along the selected axis",
|
||||
},
|
||||
{
|
||||
label: "std",
|
||||
type: "method",
|
||||
info: "Return sample standard deviation over requested axis",
|
||||
},
|
||||
{
|
||||
label: "var",
|
||||
type: "method",
|
||||
info: "Return unbiased variance over requested axis",
|
||||
},
|
||||
{
|
||||
label: "min",
|
||||
type: "method",
|
||||
info: "Return the minimum of the values for the requested axis",
|
||||
},
|
||||
{
|
||||
label: "max",
|
||||
type: "method",
|
||||
info: "Return the maximum of the values for the requested axis",
|
||||
},
|
||||
{
|
||||
label: "count",
|
||||
type: "method",
|
||||
info: "Count non-NA cells for each column or row",
|
||||
},
|
||||
{
|
||||
label: "nunique",
|
||||
type: "method",
|
||||
info: "Count number of distinct elements in specified axis",
|
||||
},
|
||||
{
|
||||
label: "value_counts",
|
||||
type: "method",
|
||||
info: "Return a Series containing counts of unique values",
|
||||
},
|
||||
{
|
||||
label: "corr",
|
||||
type: "method",
|
||||
info: "Compute pairwise correlation of columns",
|
||||
},
|
||||
{
|
||||
label: "cov",
|
||||
type: "method",
|
||||
info: "Compute pairwise covariance of columns",
|
||||
},
|
||||
{
|
||||
label: "rank",
|
||||
type: "method",
|
||||
info: "Compute numerical data ranks (1 through n) along axis",
|
||||
},
|
||||
{
|
||||
label: "quantile",
|
||||
type: "method",
|
||||
info: "Return values at the given quantile over requested axis",
|
||||
},
|
||||
{
|
||||
label: "clip",
|
||||
type: "method",
|
||||
info: "Trim values at input threshold(s)",
|
||||
},
|
||||
|
||||
// Merging/Joining
|
||||
{
|
||||
label: "merge",
|
||||
type: "method",
|
||||
info: "Merge DataFrame or named Series objects",
|
||||
},
|
||||
{
|
||||
label: "join",
|
||||
type: "method",
|
||||
info: "Join columns of another DataFrame",
|
||||
},
|
||||
{
|
||||
label: "concat",
|
||||
type: "method",
|
||||
info: "Concatenate pandas objects along a particular axis",
|
||||
},
|
||||
{
|
||||
label: "append",
|
||||
type: "method",
|
||||
info: "Append rows of other to the end of caller",
|
||||
},
|
||||
{
|
||||
label: "update",
|
||||
type: "method",
|
||||
info: "Modify in place using non-NA values from another DataFrame",
|
||||
},
|
||||
|
||||
// Reshaping
|
||||
{
|
||||
label: "pivot",
|
||||
type: "method",
|
||||
info: "Return reshaped DataFrame organized by given index / column values",
|
||||
},
|
||||
{
|
||||
label: "pivot_table",
|
||||
type: "method",
|
||||
info: "Create a spreadsheet-style pivot table",
|
||||
},
|
||||
{
|
||||
label: "melt",
|
||||
type: "method",
|
||||
info: "Unpivot a DataFrame from wide to long format",
|
||||
},
|
||||
{
|
||||
label: "stack",
|
||||
type: "method",
|
||||
info: "Stack the prescribed level(s) from columns to index",
|
||||
},
|
||||
{
|
||||
label: "unstack",
|
||||
type: "method",
|
||||
info: "Pivot a level of the (necessarily hierarchical) index labels",
|
||||
},
|
||||
{ label: "transpose", type: "method", info: "Transpose index and columns" },
|
||||
{ label: "T", type: "property", info: "Transpose index and columns" },
|
||||
|
||||
// I/O
|
||||
{
|
||||
label: "to_csv",
|
||||
type: "method",
|
||||
info: "Write object to a comma-separated values (csv) file",
|
||||
},
|
||||
{
|
||||
label: "to_excel",
|
||||
type: "method",
|
||||
info: "Write object to an Excel sheet",
|
||||
},
|
||||
{
|
||||
label: "to_json",
|
||||
type: "method",
|
||||
info: "Convert the object to a JSON string",
|
||||
},
|
||||
{
|
||||
label: "to_sql",
|
||||
type: "method",
|
||||
info: "Write records stored in a DataFrame to a SQL database",
|
||||
},
|
||||
{
|
||||
label: "to_parquet",
|
||||
type: "method",
|
||||
info: "Write a DataFrame to the binary parquet format",
|
||||
},
|
||||
{
|
||||
label: "to_dict",
|
||||
type: "method",
|
||||
info: "Convert the DataFrame to a dictionary",
|
||||
},
|
||||
{
|
||||
label: "to_html",
|
||||
type: "method",
|
||||
info: "Render a DataFrame as an HTML table",
|
||||
},
|
||||
{
|
||||
label: "to_string",
|
||||
type: "method",
|
||||
info: "Render a DataFrame to a console-friendly tabular output",
|
||||
},
|
||||
{
|
||||
label: "to_numpy",
|
||||
type: "method",
|
||||
info: "Convert the DataFrame to a NumPy array",
|
||||
},
|
||||
|
||||
// Time Series
|
||||
{
|
||||
label: "shift",
|
||||
type: "method",
|
||||
info: "Shift index by desired number of periods",
|
||||
},
|
||||
{ label: "tshift", type: "method", info: "Shift the time index" },
|
||||
{
|
||||
label: "diff",
|
||||
type: "method",
|
||||
info: "First discrete difference of element",
|
||||
},
|
||||
{
|
||||
label: "pct_change",
|
||||
type: "method",
|
||||
info: "Percentage change between the current and a prior element",
|
||||
},
|
||||
|
||||
// Plotting (if matplotlib available)
|
||||
{
|
||||
label: "plot",
|
||||
type: "method",
|
||||
info: "Make plots of Series or DataFrame",
|
||||
},
|
||||
{
|
||||
label: "hist",
|
||||
type: "method",
|
||||
info: "Make a histogram of the DataFrame's columns",
|
||||
},
|
||||
{
|
||||
label: "boxplot",
|
||||
type: "method",
|
||||
info: "Make a box plot of the DataFrame columns",
|
||||
},
|
||||
],
|
||||
|
||||
// Series methods (subset of DataFrame methods that make sense for Series)
|
||||
Series: [
|
||||
{ label: "head", type: "method", info: "Return the first n values" },
|
||||
{ label: "tail", type: "method", info: "Return the last n values" },
|
||||
{
|
||||
label: "describe",
|
||||
type: "method",
|
||||
info: "Generate descriptive statistics",
|
||||
},
|
||||
{
|
||||
label: "value_counts",
|
||||
type: "method",
|
||||
info: "Return a Series containing counts of unique values",
|
||||
},
|
||||
{
|
||||
label: "unique",
|
||||
type: "method",
|
||||
info: "Return unique values of Series object",
|
||||
},
|
||||
{
|
||||
label: "nunique",
|
||||
type: "method",
|
||||
info: "Return number of unique elements in the object",
|
||||
},
|
||||
{ label: "sort_values", type: "method", info: "Sort by the values" },
|
||||
{
|
||||
label: "sort_index",
|
||||
type: "method",
|
||||
info: "Sort Series by index labels",
|
||||
},
|
||||
{
|
||||
label: "reset_index",
|
||||
type: "method",
|
||||
info: "Generate a new DataFrame or Series with the index reset",
|
||||
},
|
||||
{
|
||||
label: "dropna",
|
||||
type: "method",
|
||||
info: "Return a new Series with missing values removed",
|
||||
},
|
||||
{
|
||||
label: "fillna",
|
||||
type: "method",
|
||||
info: "Fill NA/NaN values using the specified method",
|
||||
},
|
||||
{
|
||||
label: "apply",
|
||||
type: "method",
|
||||
info: "Invoke function on values of Series",
|
||||
},
|
||||
{
|
||||
label: "map",
|
||||
type: "method",
|
||||
info: "Map values of Series according to input correspondence",
|
||||
},
|
||||
{
|
||||
label: "replace",
|
||||
type: "method",
|
||||
info: "Replace values given in to_replace with value",
|
||||
},
|
||||
{
|
||||
label: "str",
|
||||
type: "property",
|
||||
info: "Vectorized string functions for Series and Index",
|
||||
},
|
||||
{
|
||||
label: "dt",
|
||||
type: "property",
|
||||
info: "Accessor object for datetimelike properties",
|
||||
},
|
||||
{
|
||||
label: "cat",
|
||||
type: "property",
|
||||
info: "Accessor object for categorical properties",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function genericPandasCompletions(context: CompletionContext) {
|
||||
const word = context.matchBefore(/[\w.]*/);
|
||||
if (!word || (word.from === word.to && !context.explicit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = word.text;
|
||||
|
||||
// Match patterns like "pd.", "df.", etc.
|
||||
if (text.includes(".")) {
|
||||
const parts = text.split(".");
|
||||
const obj = parts.at(-2);
|
||||
const partial = parts.at(-1) ?? "";
|
||||
|
||||
if (!obj || !pandasDefinitions[obj]) {
|
||||
return null;
|
||||
// Find the first line that is not a comment
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.trim().startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const options = pandasDefinitions[obj]
|
||||
.filter(
|
||||
(item) =>
|
||||
!partial ||
|
||||
item.label.toLowerCase().startsWith(partial.toLowerCase()),
|
||||
)
|
||||
.map((comp) => ({
|
||||
...comp,
|
||||
info: undefined,
|
||||
boost: comp.type === "method" ? 2 : comp.type === "function" ? 1 : 0,
|
||||
}));
|
||||
|
||||
return {
|
||||
filter: false,
|
||||
from: word.from + text.length - partial.length,
|
||||
options,
|
||||
};
|
||||
lines.splice(i, 0, `import ${lib}`);
|
||||
break;
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export const completion = [
|
||||
pythonLanguage.data.of({
|
||||
autocomplete: genericPandasCompletions,
|
||||
}),
|
||||
autocompletion(),
|
||||
];
|
||||
function libImportRegex(lib: string) {
|
||||
return new RegExp(`import ${lib}`, "g");
|
||||
}
|
||||
|
||||
export function removeImport(source: string, lib: string) {
|
||||
const lines = source.split("\n");
|
||||
return lines.filter((line) => !libImportRegex(lib).test(line)).join("\n");
|
||||
}
|
||||
|
||||
export function hasImport(source: string, lib: string) {
|
||||
const regex = libImportRegex(lib);
|
||||
return regex.test(source);
|
||||
}
|
||||
|
||||
@@ -6,3 +6,5 @@ export const POLLING_INTERVAL = isCypressActive ? 100 : 3000;
|
||||
|
||||
export const FILTER_WIDGET_MIN_WIDTH = 300;
|
||||
export const FILTER_WIDGET_MAX_HEIGHT = 400;
|
||||
|
||||
export const SHARED_LIB_IMPORT_NAME = "lib";
|
||||
|
||||
@@ -11,6 +11,7 @@ import { JobPage } from "./pages/JobPage";
|
||||
import { NewJobPage } from "./pages/NewJobPage";
|
||||
import { NewPythonTransformPage } from "./pages/NewPythonTransformPage";
|
||||
import { NewTransformQueryPage } from "./pages/NewTransformQueryPage";
|
||||
import { PythonLibraryEditorPage } from "./pages/PythonLibraryEditorPage";
|
||||
import { RunListPage } from "./pages/RunListPage";
|
||||
import { TransformListPage } from "./pages/TransformListPage";
|
||||
import { TransformPage } from "./pages/TransformPage";
|
||||
@@ -33,6 +34,11 @@ if (hasPremiumFeature("transforms")) {
|
||||
<Route path="runs" component={RunListPage} />
|
||||
<Route path=":transformId" component={TransformPage} />
|
||||
</Route>
|
||||
<Route
|
||||
component={(props) => <TransformPageLayout {...props} fullWidth />}
|
||||
>
|
||||
<Route path="library/:name" component={PythonLibraryEditorPage} />
|
||||
</Route>
|
||||
<Route
|
||||
path="new/:type"
|
||||
component={(props: any) =>
|
||||
|
||||
@@ -20,7 +20,7 @@ import pandas as pd
|
||||
def transform():
|
||||
"""
|
||||
Your transformation function.
|
||||
|
||||
|
||||
Select tables above to add them as function parameters.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.editor {
|
||||
/* HACK: 133px is the height of the headers */
|
||||
height: calc(100vh - 133px);
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import Button from "metabase/common/components/Button";
|
||||
import EditBar from "metabase/common/components/EditBar";
|
||||
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
|
||||
import { useMetadataToasts } from "metabase/metadata/hooks";
|
||||
import { Box, Flex } from "metabase/ui";
|
||||
import {
|
||||
useGetPythonLibraryQuery,
|
||||
useUpdatePythonLibraryMutation,
|
||||
} from "metabase-enterprise/api/python-transform-library";
|
||||
import { PythonEditor } from "metabase-enterprise/transforms/components/PythonEditor";
|
||||
import type { PythonLibraryEditorPageParams } from "metabase-enterprise/transforms/types";
|
||||
|
||||
import S from "./PythonLibraryEditorPage.module.css";
|
||||
|
||||
type PythonLibraryEditorPageProps = {
|
||||
params: PythonLibraryEditorPageParams;
|
||||
};
|
||||
|
||||
export function PythonLibraryEditorPage({
|
||||
params,
|
||||
}: PythonLibraryEditorPageProps) {
|
||||
const { name } = params;
|
||||
const [source, setSource] = useState("");
|
||||
|
||||
const {
|
||||
data: library,
|
||||
isLoading,
|
||||
error,
|
||||
} = useGetPythonLibraryQuery({ name });
|
||||
const [updatePythonLibrary, { isLoading: isSaving }] =
|
||||
useUpdatePythonLibraryMutation();
|
||||
const { sendSuccessToast, sendErrorToast } = useMetadataToasts();
|
||||
|
||||
useEffect(() => {
|
||||
setSource(library?.source || "");
|
||||
}, [library?.source]);
|
||||
|
||||
const isDirty = source !== library?.source;
|
||||
|
||||
function handleRevert() {
|
||||
setSource(library?.source || "");
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await updatePythonLibrary({ name, source }).unwrap();
|
||||
sendSuccessToast(t`Python library saved`);
|
||||
} catch (error) {
|
||||
sendErrorToast(t`Python library could not be saved`);
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading || error) {
|
||||
return (
|
||||
<Box p="md">
|
||||
<LoadingAndErrorWrapper loading={isLoading} error={error} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex h="100%" w="100%" bg="bg-light" gap={0} direction="column">
|
||||
<LibraryEditorHeader
|
||||
onSave={handleSave}
|
||||
onRevert={handleRevert}
|
||||
isDirty={isDirty}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
<PythonEditor
|
||||
value={source}
|
||||
onChange={setSource}
|
||||
withPandasCompletions
|
||||
className={S.editor}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export function LibraryEditorHeader({
|
||||
isDirty,
|
||||
isSaving,
|
||||
onSave,
|
||||
onRevert,
|
||||
}: {
|
||||
isDirty?: boolean;
|
||||
isSaving?: boolean;
|
||||
onSave: () => void;
|
||||
onRevert: () => void;
|
||||
}) {
|
||||
return (
|
||||
<EditBar
|
||||
title={t`You are editing the shared Python library`}
|
||||
admin
|
||||
buttons={[
|
||||
<Button
|
||||
key="save"
|
||||
onClick={onRevert}
|
||||
primary
|
||||
small
|
||||
disabled={!isDirty || isSaving}
|
||||
>
|
||||
{t`Revert`}
|
||||
</Button>,
|
||||
<Button
|
||||
key="save"
|
||||
onClick={onSave}
|
||||
primary
|
||||
small
|
||||
disabled={!isDirty || isSaving}
|
||||
>
|
||||
{t`Save`}
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./PythonLibraryEditorPage";
|
||||
@@ -8,8 +8,14 @@ import {
|
||||
import { AdminSettingsLayout } from "metabase/common/components/AdminLayout/AdminSettingsLayout";
|
||||
import { useSelector } from "metabase/lib/redux";
|
||||
import { getLocation } from "metabase/selectors/routing";
|
||||
import { SHARED_LIB_IMPORT_NAME } from "metabase-enterprise/transforms/constants";
|
||||
|
||||
import { getJobListUrl, getRunListUrl, getTransformListUrl } from "../../urls";
|
||||
import {
|
||||
getJobListUrl,
|
||||
getPythonLibraryUrl,
|
||||
getRunListUrl,
|
||||
getTransformListUrl,
|
||||
} from "../../urls";
|
||||
|
||||
type TransformPageLayoutParams = {
|
||||
transformId?: string;
|
||||
@@ -18,17 +24,20 @@ type TransformPageLayoutParams = {
|
||||
|
||||
type TransformPageLayoutProps = {
|
||||
params: TransformPageLayoutParams;
|
||||
fullWidth?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function TransformPageLayout({
|
||||
params,
|
||||
children,
|
||||
fullWidth,
|
||||
}: TransformPageLayoutProps) {
|
||||
return (
|
||||
<AdminSettingsLayout
|
||||
sidebar={<TransformSidebar params={params} />}
|
||||
maw="60rem"
|
||||
fullWidth={fullWidth}
|
||||
>
|
||||
{children}
|
||||
</AdminSettingsLayout>
|
||||
@@ -61,6 +70,11 @@ function TransformSidebar({ params }: TransformSidebarProps) {
|
||||
active={pathname?.startsWith(jobListUrl)}
|
||||
/>
|
||||
<AdminNavItem label={t`Runs`} path={getRunListUrl()} icon="list" />
|
||||
<AdminNavItem
|
||||
label={t`Python library`}
|
||||
path={getPythonLibraryUrl({ name: SHARED_LIB_IMPORT_NAME })}
|
||||
icon="code_block"
|
||||
/>
|
||||
</AdminNavWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,3 +20,7 @@ export type JobListParams = {
|
||||
nextRunStartTime?: string;
|
||||
transformTagIds?: TransformTagId[];
|
||||
};
|
||||
|
||||
export type PythonLibraryEditorPageParams = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
@@ -7,7 +7,11 @@ import type {
|
||||
TransformJobId,
|
||||
} from "metabase-types/api";
|
||||
|
||||
import type { JobListParams, RunListParams } from "./types";
|
||||
import type {
|
||||
JobListParams,
|
||||
PythonLibraryEditorPageParams,
|
||||
RunListParams,
|
||||
} from "./types";
|
||||
|
||||
export const ROOT_URL = "/admin/transforms";
|
||||
|
||||
@@ -123,3 +127,7 @@ export function getTableMetadataUrl(
|
||||
) {
|
||||
return `/admin/datamodel/database/${databaseId}/schema/${databaseId}:${encodeURIComponent(schema ?? "")}/table/${tableId}`;
|
||||
}
|
||||
|
||||
export function getPythonLibraryUrl({ name }: PythonLibraryEditorPageParams) {
|
||||
return `/admin/transforms/library/${name}`;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
background-color: var(--mb-color-bg-light);
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ import S from "./AdminSettingsLayout.module.css";
|
||||
export const AdminSettingsLayout = ({
|
||||
sidebar,
|
||||
children,
|
||||
fullWidth = false,
|
||||
maw = "50rem",
|
||||
}: {
|
||||
sidebar?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
maw?: string;
|
||||
fullWidth?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Box className={S.Wrapper}>
|
||||
@@ -24,9 +26,13 @@ export const AdminSettingsLayout = ({
|
||||
{sidebar}
|
||||
</Box>
|
||||
)}
|
||||
<Box className={S.Content} data-testid="admin-layout-content">
|
||||
<Box maw={maw} w="100%">
|
||||
<Box pb="2rem">
|
||||
<Box
|
||||
className={S.Content}
|
||||
data-testid="admin-layout-content"
|
||||
p={fullWidth ? 0 : "2rem"}
|
||||
>
|
||||
<Box maw={fullWidth ? undefined : maw} w="100%">
|
||||
<Box pb={fullWidth ? 0 : "2rem"}>
|
||||
<ErrorBoundary>{children ?? <NotFound />}</ErrorBoundary>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user