Compare commits
358 Commits
python-sec
...
feature/wo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55c5ded341 | ||
|
|
3110fbbad4 | ||
|
|
8d80ea4745 | ||
|
|
5ddda06d19 | ||
|
|
af1f08d495 | ||
|
|
9edccfcda7 | ||
|
|
ae1413f78a | ||
|
|
17711d1948 | ||
|
|
d0091b7a35 | ||
|
|
39007c80e3 | ||
|
|
6172456495 | ||
|
|
f02c1f3032 | ||
|
|
9cfbb06022 | ||
|
|
851a46ef08 | ||
|
|
d15a88abb1 | ||
|
|
bf3126f5fd | ||
|
|
7fab07f8a7 | ||
|
|
4d43b1fd76 | ||
|
|
180ab8289e | ||
|
|
c77a7d2c6e | ||
|
|
0c2492ca2d | ||
|
|
f32e2ca250 | ||
|
|
c29943cc6e | ||
|
|
9d50ac2b2c | ||
|
|
6b03084603 | ||
|
|
3a2fd8dec5 | ||
|
|
aa318f3c3f | ||
|
|
46451c391b | ||
|
|
fc356df795 | ||
|
|
7ef6084b82 | ||
|
|
c2311a937c | ||
|
|
eac016b6e0 | ||
|
|
63c6cb2e8a | ||
|
|
c91abe2289 | ||
|
|
ccbcc9d827 | ||
|
|
6f23b3628c | ||
|
|
754c46ccff | ||
|
|
2435844a0a | ||
|
|
a8233878b1 | ||
|
|
51eea03918 | ||
|
|
3d257d9cb8 | ||
|
|
2ab7a09e74 | ||
|
|
44e7b1fec4 | ||
|
|
137dc1f6c0 | ||
|
|
b1d86a1f64 | ||
|
|
fdaa2725e2 | ||
|
|
918ee7d967 | ||
|
|
8616e0201b | ||
|
|
093ff50138 | ||
|
|
04639e0f89 | ||
|
|
8903ee8503 | ||
|
|
a2da9a591d | ||
|
|
ce64753799 | ||
|
|
0787aba9df | ||
|
|
19f0abe663 | ||
|
|
7cb77b683d | ||
|
|
c1eaec238d | ||
|
|
bd19df717b | ||
|
|
819df161e8 | ||
|
|
f0e235a9a1 | ||
|
|
a5acb175bf | ||
|
|
50bc335216 | ||
|
|
1eedf421ec | ||
|
|
35ea73bafa | ||
|
|
e50f7eebac | ||
|
|
4e48c521cc | ||
|
|
d369ae87a2 | ||
|
|
2fd96a5131 | ||
|
|
69e1f4b44f | ||
|
|
4b6f6720b8 | ||
|
|
004b5e19aa | ||
|
|
d6a7827fdb | ||
|
|
8dd754914e | ||
|
|
cb8107c889 | ||
|
|
c46f10cca9 | ||
|
|
600a5adc60 | ||
|
|
d8d4aa62c6 | ||
|
|
e8d0d7ccf0 | ||
|
|
8a94afe24e | ||
|
|
8b7b9f23ee | ||
|
|
846a06d468 | ||
|
|
334f6764a4 | ||
|
|
2d238a59b8 | ||
|
|
aebc2f9cec | ||
|
|
ab3ad35169 | ||
|
|
741c5141a1 | ||
|
|
66d0ca33a1 | ||
|
|
2a77406380 | ||
|
|
d82859096b | ||
|
|
a4d54730a7 | ||
|
|
cde71d4e7b | ||
|
|
7ba374ed2c | ||
|
|
9ff08e435e | ||
|
|
66bbc46041 | ||
|
|
fa39c3f3a2 | ||
|
|
1f47cabe12 | ||
|
|
2a909a7331 | ||
|
|
38f2183fd8 | ||
|
|
153b7d2e6b | ||
|
|
8fae4a1b53 | ||
|
|
eb78a56002 | ||
|
|
fb10eca5ec | ||
|
|
2f73e2a313 | ||
|
|
7e916af416 | ||
|
|
143c15fe3a | ||
|
|
41effadd03 | ||
|
|
60959aab28 | ||
|
|
39bf0a2acd | ||
|
|
bfcbe96c25 | ||
|
|
6a809f57df | ||
|
|
5cc67c9837 | ||
|
|
f50caba00a | ||
|
|
17e7061d65 | ||
|
|
afcd00937c | ||
|
|
25110a9c64 | ||
|
|
7daae384a4 | ||
|
|
72545f91f8 | ||
|
|
84b4ed11e5 | ||
|
|
a460095faa | ||
|
|
529e8c0918 | ||
|
|
4a684a69eb | ||
|
|
10c2ddf94d | ||
|
|
3558477a00 | ||
|
|
f22ba1f492 | ||
|
|
7ebccaee5a | ||
|
|
4c28250828 | ||
|
|
00f5258ae9 | ||
|
|
240f4e7c05 | ||
|
|
64a60cf3fd | ||
|
|
ab7955a71c | ||
|
|
3bc02091d3 | ||
|
|
c4c1197ad9 | ||
|
|
30c6870162 | ||
|
|
2c64f9762f | ||
|
|
de9fab0a69 | ||
|
|
188e1ed86b | ||
|
|
26fe968cc4 | ||
|
|
a8709f8e67 | ||
|
|
ab5fd38545 | ||
|
|
52839f51f5 | ||
|
|
ba366d0fc3 | ||
|
|
d4e48d9364 | ||
|
|
f8a044be14 | ||
|
|
28967cc943 | ||
|
|
069efdb40a | ||
|
|
3db8123705 | ||
|
|
42ba3f9c14 | ||
|
|
4ab4f51396 | ||
|
|
495c3688ea | ||
|
|
921dfcf21a | ||
|
|
a15d84f2f6 | ||
|
|
791d521c1d | ||
|
|
562447cb8b | ||
|
|
4648e5ce4e | ||
|
|
4ec14ba0cb | ||
|
|
7b660ecf97 | ||
|
|
8b280b2e0f | ||
|
|
b36f48841f | ||
|
|
9381eeeef7 | ||
|
|
cc207ba264 | ||
|
|
4924a10623 | ||
|
|
5f62970982 | ||
|
|
a965b27c76 | ||
|
|
e791b25304 | ||
|
|
bb3bb67bab | ||
|
|
1c2c2bec0d | ||
|
|
7d2de7b026 | ||
|
|
7e08851537 | ||
|
|
4ba305e11c | ||
|
|
24cdd2b5b9 | ||
|
|
fff42a67f9 | ||
|
|
b39ab269b5 | ||
|
|
86e00e29ac | ||
|
|
acd131aad5 | ||
|
|
c9f7077f67 | ||
|
|
2f04965b1c | ||
|
|
acceeb354d | ||
|
|
a9a5d21b76 | ||
|
|
d81b2f8a7f | ||
|
|
53fd65a225 | ||
|
|
4f540fd2a5 | ||
|
|
fe60ba8212 | ||
|
|
2c2054cab9 | ||
|
|
17c4c33396 | ||
|
|
714e95869b | ||
|
|
5135164b66 | ||
|
|
967c4d7cd1 | ||
|
|
b7408de5f4 | ||
|
|
9bf024ae3c | ||
|
|
458e7a698b | ||
|
|
a60b61696e | ||
|
|
f16ca27e27 | ||
|
|
a73733429e | ||
|
|
06f9f8a915 | ||
|
|
2d311a0041 | ||
|
|
1f7eeb5f73 | ||
|
|
a2d8aa365b | ||
|
|
88d153e3dd | ||
|
|
143c741be0 | ||
|
|
46cd2d6776 | ||
|
|
fede88fb6c | ||
|
|
492f70de28 | ||
|
|
4345336b8b | ||
|
|
ac1b7821f8 | ||
|
|
467a9f6c3a | ||
|
|
595c0c470c | ||
|
|
7c2c016c49 | ||
|
|
51f2ba9afb | ||
|
|
e92daf3345 | ||
|
|
1b38a2ede9 | ||
|
|
714dd5a28a | ||
|
|
7925f15150 | ||
|
|
e156483792 | ||
|
|
eed8e62a40 | ||
|
|
de1da8ea36 | ||
|
|
66edb69b49 | ||
|
|
bf51f83ef9 | ||
|
|
f3375d4c73 | ||
|
|
167acd21c1 | ||
|
|
99378d6545 | ||
|
|
7d2a4f80b4 | ||
|
|
899e60782a | ||
|
|
0d26c3a39b | ||
|
|
eb4ed5164e | ||
|
|
5319abd9e0 | ||
|
|
7f7fb5ca23 | ||
|
|
7a18fcdd72 | ||
|
|
b453743dfd | ||
|
|
4156768ba0 | ||
|
|
830f90eb49 | ||
|
|
4c147e883e | ||
|
|
8ac8ec7393 | ||
|
|
f918750673 | ||
|
|
12f84b20ec | ||
|
|
2e8477e76a | ||
|
|
9d6c8964fd | ||
|
|
4b30b4a9db | ||
|
|
16387fc2c0 | ||
|
|
cc643ed0bc | ||
|
|
8d9d49eca7 | ||
|
|
0fc4e30ce9 | ||
|
|
164b80195a | ||
|
|
f3ba3a0c18 | ||
|
|
4328582106 | ||
|
|
ac5f1a7b33 | ||
|
|
9ccc3e6c74 | ||
|
|
e11188ba1c | ||
|
|
ad02ea5fb0 | ||
|
|
7e6b37511d | ||
|
|
6b58ccb118 | ||
|
|
b15835b64c | ||
|
|
eed61ada77 | ||
|
|
3c45858fc0 | ||
|
|
b3982f6788 | ||
|
|
db69938a46 | ||
|
|
d950320a6e | ||
|
|
0275bbcfa1 | ||
|
|
bd11f3fd3d | ||
|
|
2b9f879c54 | ||
|
|
85424dbb5e | ||
|
|
75b89bec06 | ||
|
|
1eb5126ede | ||
|
|
cbf835a844 | ||
|
|
403c81cc7f | ||
|
|
e071bdc36c | ||
|
|
b4c36549d8 | ||
|
|
10e0b2058f | ||
|
|
22bce8c93e | ||
|
|
e1f1918874 | ||
|
|
3ede797ec7 | ||
|
|
d40982ee2e | ||
|
|
f652a2dfbf | ||
|
|
2bf2599881 | ||
|
|
a1ef9454a5 | ||
|
|
5f11c86f04 | ||
|
|
9e4d935802 | ||
|
|
1112b082c3 | ||
|
|
8c42f74af8 | ||
|
|
c0e4ec81fa | ||
|
|
4f433602bc | ||
|
|
9a2190d44d | ||
|
|
d3cf46ea88 | ||
|
|
dd004902d7 | ||
|
|
0cd22badfa | ||
|
|
f08d7c996a | ||
|
|
d85ea68a4f | ||
|
|
8545180dc5 | ||
|
|
2071b499ab | ||
|
|
42f7f91247 | ||
|
|
c7ab945216 | ||
|
|
11e7f8f792 | ||
|
|
06187e258c | ||
|
|
1521f87c35 | ||
|
|
9746d26262 | ||
|
|
bde75ec8cb | ||
|
|
c386f5dc06 | ||
|
|
db9915dc19 | ||
|
|
96c5218a0e | ||
|
|
5e512b1870 | ||
|
|
d093af8894 | ||
|
|
7e54498224 | ||
|
|
83ecd08c47 | ||
|
|
bb30961bab | ||
|
|
0176fb79c3 | ||
|
|
fc63da673f | ||
|
|
83212f5a87 | ||
|
|
d47c329ed7 | ||
|
|
4c3aaac096 | ||
|
|
78e3516d32 | ||
|
|
4b483e8ac4 | ||
|
|
025f903439 | ||
|
|
47f82c8a77 | ||
|
|
9599fc0116 | ||
|
|
697426d297 | ||
|
|
942643a1a8 | ||
|
|
21870b3bdf | ||
|
|
7cf553aee0 | ||
|
|
29b6ee74a8 | ||
|
|
b4b7907cc6 | ||
|
|
18e00b770c | ||
|
|
5fb62d64b7 | ||
|
|
cd507e20a3 | ||
|
|
8c62c354ad | ||
|
|
0ae57508ec | ||
|
|
8fc39832c8 | ||
|
|
20f5fa2761 | ||
|
|
5dce588375 | ||
|
|
e92e9017b9 | ||
|
|
4515a136ff | ||
|
|
874cd510b5 | ||
|
|
7dec13db6c | ||
|
|
a840799f61 | ||
|
|
96b217181c | ||
|
|
c7831044e2 | ||
|
|
c84d7977e8 | ||
|
|
8fb2f8af83 | ||
|
|
45206f30b5 | ||
|
|
b2db725412 | ||
|
|
f089029b69 | ||
|
|
5215a1f04b | ||
|
|
ea7cc08248 | ||
|
|
fc9ed44ef4 | ||
|
|
6b50f2284a | ||
|
|
30b0e76808 | ||
|
|
625d9c357c | ||
|
|
5aed907492 | ||
|
|
b991bc7052 | ||
|
|
add9cea204 | ||
|
|
d27be47233 | ||
|
|
abf38d93bd | ||
|
|
78b7f95c52 | ||
|
|
90a338f8a7 | ||
|
|
4b8bd6e61f | ||
|
|
0934b50409 | ||
|
|
5e095f519b | ||
|
|
ba34ffd9ca | ||
|
|
5cf33d5aef | ||
|
|
b7ee9cb338 |
@@ -2052,6 +2052,7 @@
|
||||
lib
|
||||
lib-be
|
||||
models
|
||||
permissions
|
||||
premium-features
|
||||
queries
|
||||
query-processor
|
||||
|
||||
@@ -41,7 +41,7 @@ export function createTransform(
|
||||
cy.wrap(body.id).as(idAlias);
|
||||
}
|
||||
if (visitTransform) {
|
||||
cy.visit(`/admin/transforms/${body.id}`);
|
||||
cy.visit(`/bench/transforms/${body.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function createTransformJob(
|
||||
cy.wrap(body.id).as(idAlias);
|
||||
}
|
||||
if (visitTransformJob) {
|
||||
cy.visit(`/admin/transforms/jobs/${body.id}`);
|
||||
cy.visit(`/bench/transforms/jobs/${body.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ function visit({
|
||||
fieldId != null
|
||||
) {
|
||||
cy.visit(
|
||||
`/admin/datamodel/database/${databaseId}/schema/${schemaId}/table/${tableId}/field/${fieldId}`,
|
||||
`/bench/metadata/database/${databaseId}/schema/${schemaId}/table/${tableId}/field/${fieldId}`,
|
||||
);
|
||||
|
||||
if (!skipWaiting) {
|
||||
@@ -112,7 +112,7 @@ function visit({
|
||||
|
||||
if (databaseId != null && schemaId != null && tableId != null) {
|
||||
cy.visit(
|
||||
`/admin/datamodel/database/${databaseId}/schema/${schemaId}/table/${tableId}`,
|
||||
`/bench/metadata/database/${databaseId}/schema/${schemaId}/table/${tableId}`,
|
||||
);
|
||||
|
||||
if (!skipWaiting) {
|
||||
@@ -128,7 +128,7 @@ function visit({
|
||||
}
|
||||
|
||||
if (databaseId != null && schemaId != null) {
|
||||
cy.visit(`/admin/datamodel/database/${databaseId}/schema/${schemaId}`);
|
||||
cy.visit(`/bench/metadata/database/${databaseId}/schema/${schemaId}`);
|
||||
|
||||
if (!skipWaiting) {
|
||||
cy.wait([
|
||||
@@ -141,7 +141,7 @@ function visit({
|
||||
}
|
||||
|
||||
if (databaseId != null) {
|
||||
cy.visit(`/admin/datamodel/database/${databaseId}`);
|
||||
cy.visit(`/bench/metadata/database/${databaseId}`);
|
||||
|
||||
if (!skipWaiting) {
|
||||
cy.wait([
|
||||
@@ -154,7 +154,7 @@ function visit({
|
||||
return;
|
||||
}
|
||||
|
||||
cy.visit("/admin/datamodel");
|
||||
cy.visit("/bench/metadata");
|
||||
cy.wait(["@datamodel/visit/databases"]);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export function saveMetadataChanges() {
|
||||
});
|
||||
|
||||
cy.intercept("PUT", "/api/card/*").as("updateModelMetadata");
|
||||
cy.findByTestId("dataset-edit-bar").button("Save changes").click();
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
cy.wait("@updateModelMetadata");
|
||||
cy.findByTestId("dataset-edit-bar").should("not.exist");
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { createTransform } from "./api";
|
||||
import { getTableId } from "./e2e-qa-databases-helpers";
|
||||
|
||||
export function visitTransform(transformId: TransformId) {
|
||||
cy.visit(`/admin/transforms/${transformId}`);
|
||||
cy.visit(`/bench/transforms/${transformId}`);
|
||||
}
|
||||
|
||||
const WAIT_TIMEOUT = 10000;
|
||||
|
||||
@@ -130,6 +130,14 @@ export function appBar() {
|
||||
return cy.findByLabelText("Navigation bar");
|
||||
}
|
||||
|
||||
export function benchNav() {
|
||||
return cy.findByTestId("bench-nav");
|
||||
}
|
||||
|
||||
export function benchNavTitleMenu() {
|
||||
return cy.findByTestId("bench-nav-title-menu");
|
||||
}
|
||||
|
||||
export function openNavigationSidebar() {
|
||||
appBar().findByTestId("sidebar-toggle").click();
|
||||
navigationSidebar().should("be.visible");
|
||||
|
||||
@@ -57,8 +57,9 @@ describe("scenarios > admin > datamodel", () => {
|
||||
});
|
||||
|
||||
cy.findByRole("link", { name: /Segments/ }).click();
|
||||
cy.location("pathname").should("eq", "/admin/datamodel/segments");
|
||||
cy.location("pathname").should("eq", "/bench/segment");
|
||||
cy.wait("@schema");
|
||||
cy.findByRole("link", { name: /Metadata/ }).click();
|
||||
|
||||
H.DataModel.TablePicker.getTable("Reviews").click();
|
||||
H.DataModel.TableSection.getNameInput()
|
||||
@@ -66,7 +67,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
.and("have.value", "Reviews");
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${REVIEWS_ID}`,
|
||||
`/bench/metadata/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${REVIEWS_ID}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -79,7 +80,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
TablePicker.getDatabases().should("have.length", 1);
|
||||
TablePicker.getTables().should("have.length", 0);
|
||||
H.DataModel.get().findByText("Not found.").should("be.visible");
|
||||
cy.location("pathname").should("eq", "/admin/datamodel/database/54321");
|
||||
cy.location("pathname").should("eq", "/bench/metadata/database/54321");
|
||||
});
|
||||
|
||||
it("should show 404 if table does not exist", () => {
|
||||
@@ -97,7 +98,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
H.DataModel.get().findByText("Not found.").should("be.visible");
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/12345`,
|
||||
`/bench/metadata/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/12345`,
|
||||
);
|
||||
verifyTableSectionEmptyState();
|
||||
});
|
||||
@@ -118,7 +119,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
H.DataModel.get().findByText("Not found.").should("be.visible");
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}/field/12345`,
|
||||
`/bench/metadata/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}/field/12345`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -192,7 +193,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
verifyTableSectionEmptyState();
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${MYSQL_DB_ID}/schema/${MYSQL_DB_SCHEMA_ID}`,
|
||||
`/bench/metadata/database/${MYSQL_DB_ID}/schema/${MYSQL_DB_SCHEMA_ID}`,
|
||||
);
|
||||
|
||||
TablePicker.getTable("Products").click();
|
||||
@@ -200,7 +201,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
verifyFieldSectionEmptyState();
|
||||
cy.location("pathname").should((pathname) => {
|
||||
return pathname.startsWith(
|
||||
`/admin/datamodel/database/${MYSQL_DB_ID}/schema/${MYSQL_DB_SCHEMA_ID}/table/`,
|
||||
`/bench/metadata/database/${MYSQL_DB_ID}/schema/${MYSQL_DB_SCHEMA_ID}/table/`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -220,7 +221,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
verifyFieldSectionEmptyState();
|
||||
cy.location("pathname").should((pathname) => {
|
||||
return pathname.startsWith(
|
||||
`/admin/datamodel/database/${MYSQL_DB_ID}/schema/${MYSQL_DB_SCHEMA_ID}/table/`,
|
||||
`/bench/metadata/database/${MYSQL_DB_ID}/schema/${MYSQL_DB_SCHEMA_ID}/table/`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -235,13 +236,13 @@ describe("scenarios > admin > datamodel", () => {
|
||||
TablePicker.getDatabase("QA MySQL8").click();
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${MYSQL_DB_ID}/schema/${MYSQL_DB_SCHEMA_ID}`,
|
||||
`/bench/metadata/database/${MYSQL_DB_ID}/schema/${MYSQL_DB_SCHEMA_ID}`,
|
||||
);
|
||||
|
||||
TablePicker.getDatabase("QA MySQL8").click();
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${MYSQL_DB_ID}/schema/${MYSQL_DB_SCHEMA_ID}`,
|
||||
`/bench/metadata/database/${MYSQL_DB_ID}/schema/${MYSQL_DB_SCHEMA_ID}`,
|
||||
);
|
||||
|
||||
cy.log("ensure navigation to another db works");
|
||||
@@ -257,7 +258,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
cy.log("should auto-open the only schema in the only database");
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}`,
|
||||
`/bench/metadata/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}`,
|
||||
);
|
||||
verifyTableSectionEmptyState();
|
||||
TablePicker.getDatabases().should("have.length", 1);
|
||||
@@ -269,7 +270,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}`,
|
||||
`/bench/metadata/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}`,
|
||||
);
|
||||
TableSection.get().should("be.visible");
|
||||
verifyFieldSectionEmptyState();
|
||||
@@ -277,7 +278,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
TablePicker.getTable("Products").should("be.visible").click();
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${PRODUCTS_ID}`,
|
||||
`/bench/metadata/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${PRODUCTS_ID}`,
|
||||
);
|
||||
TableSection.get().should("be.visible");
|
||||
verifyFieldSectionEmptyState();
|
||||
@@ -293,7 +294,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
TablePicker.getTable("Orders").should("be.visible").click();
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}`,
|
||||
`/bench/metadata/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}`,
|
||||
);
|
||||
TableSection.getNameInput().should("have.value", "Orders");
|
||||
|
||||
@@ -318,13 +319,13 @@ describe("scenarios > admin > datamodel", () => {
|
||||
TablePicker.getDatabase("Sample Database").click();
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}`,
|
||||
`/bench/metadata/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}`,
|
||||
);
|
||||
|
||||
TablePicker.getDatabase("Sample Database").click();
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}`,
|
||||
`/bench/metadata/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -342,7 +343,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
it("should allow to navigate databases, schemas, and tables", () => {
|
||||
H.DataModel.visit();
|
||||
|
||||
cy.location("pathname").should("eq", "/admin/datamodel/database");
|
||||
cy.location("pathname").should("eq", "/bench/metadata/database");
|
||||
TablePicker.getDatabases().should("have.length", 2);
|
||||
TablePicker.getSchemas().should("have.length", 0);
|
||||
TablePicker.getTables().should("have.length", 0);
|
||||
@@ -354,7 +355,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
.click();
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${WRITABLE_DB_ID}`,
|
||||
`/bench/metadata/database/${WRITABLE_DB_ID}`,
|
||||
);
|
||||
TablePicker.getDatabases().should("have.length", 2);
|
||||
TablePicker.getSchemas().should("have.length", 2);
|
||||
@@ -366,7 +367,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
TablePicker.getSchema("Domestic").click();
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic`,
|
||||
`/bench/metadata/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic`,
|
||||
);
|
||||
TablePicker.getDatabases().should("have.length", 2);
|
||||
TablePicker.getSchemas().should("have.length", 2);
|
||||
@@ -377,7 +378,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
TablePicker.getTable("Animals").click();
|
||||
cy.location("pathname").should((pathname) => {
|
||||
return pathname.startsWith(
|
||||
`/admin/datamodel/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic/table/`,
|
||||
`/bench/metadata/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic/table/`,
|
||||
);
|
||||
});
|
||||
TableSection.getNameInput().should("have.value", "Animals");
|
||||
@@ -389,12 +390,12 @@ describe("scenarios > admin > datamodel", () => {
|
||||
);
|
||||
cy.location("pathname").should((pathname) => {
|
||||
return pathname.startsWith(
|
||||
`/admin/datamodel/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic/table/`,
|
||||
`/bench/metadata/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic/table/`,
|
||||
);
|
||||
});
|
||||
cy.location("pathname").should(
|
||||
"not.eq",
|
||||
`/admin/datamodel/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Wild`,
|
||||
`/bench/metadata/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Wild`,
|
||||
);
|
||||
TablePicker.getDatabases().should("have.length", 2);
|
||||
TablePicker.getSchemas().should("have.length", 2);
|
||||
@@ -405,7 +406,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
TablePicker.getTable("Birds").click();
|
||||
cy.location("pathname").should((pathname) => {
|
||||
return pathname.startsWith(
|
||||
`/admin/datamodel/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic/table/`,
|
||||
`/bench/metadata/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic/table/`,
|
||||
);
|
||||
});
|
||||
TableSection.getNameInput().should("have.value", "Birds");
|
||||
@@ -426,7 +427,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
cy.log("we still have a table opened");
|
||||
cy.location("pathname").should((pathname) => {
|
||||
return pathname.startsWith(
|
||||
`/admin/datamodel/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic/table/`,
|
||||
`/bench/metadata/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic/table/`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -438,21 +439,21 @@ describe("scenarios > admin > datamodel", () => {
|
||||
.and(
|
||||
"have.attr",
|
||||
"href",
|
||||
`/admin/datamodel/database/${WRITABLE_DB_ID}`,
|
||||
`/bench/metadata/database/${WRITABLE_DB_ID}`,
|
||||
);
|
||||
TablePicker.getSchema("Domestic")
|
||||
.should("have.prop", "tagName", "A")
|
||||
.and(
|
||||
"have.attr",
|
||||
"href",
|
||||
`/admin/datamodel/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic`,
|
||||
`/bench/metadata/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Domestic`,
|
||||
);
|
||||
TablePicker.getTable("Orders")
|
||||
.should("have.prop", "tagName", "A")
|
||||
.and(
|
||||
"have.attr",
|
||||
"href",
|
||||
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}`,
|
||||
`/bench/metadata/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}/table/${ORDERS_ID}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -474,7 +475,7 @@ describe("scenarios > admin > datamodel", () => {
|
||||
|
||||
cy.location("pathname").should((pathname) => {
|
||||
return pathname.startsWith(
|
||||
`/admin/datamodel/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Wild/table/`,
|
||||
`/bench/metadata/database/${WRITABLE_DB_ID}/schema/${WRITABLE_DB_ID}:Wild/table/`,
|
||||
);
|
||||
});
|
||||
TableSection.getNameInput().should("have.value", "Birds");
|
||||
|
||||
@@ -12,12 +12,7 @@ describe("scenarios > admin > datamodel > segments", () => {
|
||||
|
||||
describe("with no segments", () => {
|
||||
it("should have 'Custom expression' in a filter list (metabase#13069)", () => {
|
||||
cy.visit("/admin/datamodel/segments");
|
||||
|
||||
cy.log("should initially show no segments in UI");
|
||||
cy.get("main").findByText(
|
||||
"Create segments to add them to the Filter dropdown in the query builder",
|
||||
);
|
||||
cy.visit("/bench/segment");
|
||||
|
||||
cy.button("New segment").click();
|
||||
|
||||
@@ -104,21 +99,18 @@ describe("scenarios > admin > datamodel > segments", () => {
|
||||
});
|
||||
|
||||
it("should show up in UI list and should show the segment details of a specific id", () => {
|
||||
cy.visit("/admin/datamodel/segments");
|
||||
cy.visit("/bench/segment");
|
||||
|
||||
cy.findByRole("table").within(() => {
|
||||
cy.findByText("Filtered by Total is less than 100").should(
|
||||
"be.visible",
|
||||
);
|
||||
cy.findByRole("menu").within(() => {
|
||||
cy.findByText("Sample Database").should("be.visible");
|
||||
cy.findByText("Orders").should("be.visible");
|
||||
});
|
||||
cy.findByRole("link", { name: /Orders < 100/ })
|
||||
cy.findByRole("menuitem", { name: /Orders < 100/ })
|
||||
.should("be.visible")
|
||||
.click();
|
||||
|
||||
cy.get("form").within(() => {
|
||||
cy.findByText("Edit Your Segment").should("be.visible");
|
||||
cy.findByText("Edit your segment").should("be.visible");
|
||||
cy.findByText("Sample Database").should("be.visible");
|
||||
cy.findByText("Orders").should("be.visible");
|
||||
});
|
||||
@@ -163,8 +155,7 @@ describe("scenarios > admin > datamodel > segments", () => {
|
||||
});
|
||||
|
||||
it("should update that segment", () => {
|
||||
cy.visit("/admin");
|
||||
cy.findByTestId("admin-navbar-items").contains("Table Metadata").click();
|
||||
cy.visit("/bench");
|
||||
cy.findByRole("link", { name: /Segments/ }).click();
|
||||
|
||||
cy.findByTestId("segment-list-app")
|
||||
@@ -178,7 +169,7 @@ describe("scenarios > admin > datamodel > segments", () => {
|
||||
|
||||
// update the filter from "< 100" to "> 10"
|
||||
cy.url().should("match", /segment\/1$/);
|
||||
cy.get("label").contains("Edit Your Segment");
|
||||
cy.get("div").contains("Edit your segment");
|
||||
cy.findByTestId("filter-pill")
|
||||
.contains(/Total\s+is less than/)
|
||||
.click();
|
||||
@@ -196,10 +187,10 @@ describe("scenarios > admin > datamodel > segments", () => {
|
||||
.clear()
|
||||
.type("All orders with a total over $10.");
|
||||
cy.get('[name="revision_message"]').type("time for a change");
|
||||
cy.findByTestId("field-set-content").findByText("Save changes").click();
|
||||
cy.findByRole("button", { name: "Save changes" }).click();
|
||||
|
||||
// get redirected to previous page and see the new segment name
|
||||
cy.url().should("match", /datamodel\/segments$/);
|
||||
cy.url().should("match", /bench\/segment$/);
|
||||
|
||||
cy.findByTestId("segment-list-app").findByText("Orders > 10");
|
||||
|
||||
@@ -235,34 +226,27 @@ describe("scenarios > admin > datamodel > segments", () => {
|
||||
cy.log(
|
||||
"Make sure revisions are displayed properly in admin table metadata",
|
||||
);
|
||||
cy.visit("/admin/datamodel/segments");
|
||||
cy.get("tr")
|
||||
cy.visit("/bench/segment");
|
||||
cy.get("li")
|
||||
.filter(`:contains(${SEGMENT_NAME})`)
|
||||
.icon("ellipsis")
|
||||
.click();
|
||||
H.popover().findByTextEnsureVisible("Revision History").click();
|
||||
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
"/admin/datamodel/segment/1/revisions",
|
||||
);
|
||||
cy.location("pathname").should("eq", "/bench/segment/1/revisions");
|
||||
|
||||
cy.findByTestId("segment-revisions").within(() => {
|
||||
// metabase#45594
|
||||
cy.findByRole("heading", {
|
||||
name: `Revision History for "${SEGMENT_NAME}"`,
|
||||
name: `Revision history for "${SEGMENT_NAME}"`,
|
||||
}).should("be.visible");
|
||||
|
||||
assertRevisionHistory();
|
||||
});
|
||||
|
||||
cy.findByTestId("breadcrumbs").within(() => {
|
||||
cy.findByText("Segment History");
|
||||
cy.findByRole("link", { name: "Segments" }).click();
|
||||
});
|
||||
cy.findByRole("link", { name: /Segments/ }).click();
|
||||
|
||||
cy.location("pathname").should("eq", "/admin/datamodel/segments");
|
||||
cy.location("search").should("eq", `?table=${ORDERS_ID}`);
|
||||
cy.location("pathname").should("eq", "/bench/segment");
|
||||
|
||||
function assertRevisionHistory() {
|
||||
cy.findAllByRole("listitem").as("revisions").should("have.length", 2);
|
||||
|
||||
@@ -1794,7 +1794,7 @@ LIMIT
|
||||
);
|
||||
|
||||
function visitCommonLibrary(path = "common.py") {
|
||||
cy.visit(`/admin/transforms/library/${path}`);
|
||||
cy.visit(`/bench/transforms/library/${path}`);
|
||||
}
|
||||
|
||||
function getLibraryEditorHeader() {
|
||||
@@ -2808,15 +2808,15 @@ function getEndAtFilterWidget() {
|
||||
}
|
||||
|
||||
function visitTransformListPage() {
|
||||
return cy.visit("/admin/transforms");
|
||||
return cy.visit("/bench/transforms");
|
||||
}
|
||||
|
||||
function visitJobListPage() {
|
||||
return cy.visit("/admin/transforms/jobs");
|
||||
return cy.visit("/bench/jobs");
|
||||
}
|
||||
|
||||
function visitRunListPage() {
|
||||
return cy.visit("/admin/transforms/runs");
|
||||
return cy.visit("/bench/runs");
|
||||
}
|
||||
|
||||
function runTransformAndWaitForSuccess() {
|
||||
|
||||
@@ -155,7 +155,7 @@ describe("scenarios > dependencies > dependency graph", () => {
|
||||
});
|
||||
testEntitySearch({
|
||||
itemName: TABLE_BASED_TRANSFORM_NAME,
|
||||
itemIcon: "refresh_downstream",
|
||||
itemIcon: "transform",
|
||||
isRecentItem: false,
|
||||
});
|
||||
});
|
||||
@@ -195,7 +195,7 @@ describe("scenarios > dependencies > dependency graph", () => {
|
||||
tabName: "Transforms",
|
||||
itemName: TABLE_BASED_TRANSFORM_NAME,
|
||||
itemLevel: 0,
|
||||
itemIcon: "refresh_downstream",
|
||||
itemIcon: "transform",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1159,18 +1159,19 @@ describe("issue 21528", () => {
|
||||
H.popover().contains("Aerodynamic Bronze Hat - 144").should("be.visible");
|
||||
|
||||
cy.log("The following scenario breaks on 46");
|
||||
// Navigating to another page via JavaScript is faster than using `cy.visit("/admin/datamodel")` to load the whole page again.
|
||||
// Navigating to another page via JavaScript is faster than using `cy.visit("/bench/metadata")` to load the whole page again.
|
||||
H.appBar().findByRole("button", { name: "Settings" }).click();
|
||||
H.popover().findByText("Admin settings").click();
|
||||
H.appBar().findByText("Table Metadata").click();
|
||||
H.popover().findByText("Workbench").click();
|
||||
H.benchNav().findByText("Metadata").click();
|
||||
cy.findByRole("main")
|
||||
.findByText("Start by selecting data to model")
|
||||
.should("be.visible");
|
||||
cy.location("pathname").should(
|
||||
"eq",
|
||||
`/admin/datamodel/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}`,
|
||||
`/bench/metadata/database/${SAMPLE_DB_ID}/schema/${SAMPLE_DB_SCHEMA_ID}`,
|
||||
);
|
||||
cy.findByTestId("admin-navbar").findByText("Exit admin").click();
|
||||
H.benchNavTitleMenu().click();
|
||||
H.popover().findByText("Explore").click();
|
||||
|
||||
H.openNavigationSidebar();
|
||||
H.navigationSidebar().findByText("Our analytics").click();
|
||||
|
||||
@@ -7,7 +7,7 @@ const { H } = cy;
|
||||
const SOURCE_TABLE = "Animals";
|
||||
|
||||
// Helper functions for metabot transform interactions
|
||||
const visitTransformListPage = () => cy.visit("/admin/transforms");
|
||||
const visitTransformListPage = () => cy.visit("/bench/transforms");
|
||||
|
||||
const suggestions = () => cy.findAllByTestId("metabot-chat-suggestion");
|
||||
// eslint-disable-next-line no-unsafe-element-filtering
|
||||
@@ -47,10 +47,17 @@ const assertSuggestionInSidebar = (values: {
|
||||
}
|
||||
};
|
||||
|
||||
const assertEditorDiffState = (opts: { exists: boolean }) => {
|
||||
const assertEditorDiffState = (
|
||||
editorType: EditorType,
|
||||
opts: { exists: boolean },
|
||||
) => {
|
||||
const should = opts.exists ? "exist" : "not.exist";
|
||||
cy.findByRole("button", { name: /apply|create/i }).should(should);
|
||||
cy.findByRole("button", { name: /reject/i }).should(should);
|
||||
editor(editorType)
|
||||
.get()
|
||||
.within(() => {
|
||||
cy.findByRole("button", { name: /apply|create/i }).should(should);
|
||||
cy.findByRole("button", { name: /reject/i }).should(should);
|
||||
});
|
||||
};
|
||||
|
||||
const assertAcceptRejectUI = (opts: { visible: boolean }) => {
|
||||
@@ -93,8 +100,8 @@ H.describeWithSnowplowEE("scenarios > metabot > transforms codegen", () => {
|
||||
|
||||
cy.log("Should be able to visit and the see new transform");
|
||||
viewLastSuggestion();
|
||||
cy.url().should("include", "/admin/transforms/new/native");
|
||||
assertEditorDiffState({ exists: false }); // nothing to diff, so we shouldn't show the UI for it
|
||||
cy.url().should("include", "/bench/transforms/new/native");
|
||||
assertEditorDiffState("native", { exists: false }); // nothing to diff, so we shouldn't show the UI for it
|
||||
assertEditorContent("native", "SELECT 1");
|
||||
|
||||
// User should ask metabot for a change
|
||||
@@ -165,8 +172,8 @@ H.describeWithSnowplowEE("scenarios > metabot > transforms codegen", () => {
|
||||
|
||||
cy.log("Should be able to visit and the see new transform");
|
||||
viewLastSuggestion();
|
||||
cy.url().should("include", "/admin/transforms/new/python");
|
||||
assertEditorDiffState({ exists: false }); // nothing to diff, so we shouldn't show the UI for it
|
||||
cy.url().should("include", "/bench/transforms/new/python");
|
||||
assertEditorDiffState("python", { exists: false }); // nothing to diff, so we shouldn't show the UI for it
|
||||
assertEditorContent("python", "pd.DataFrame({'value': [1]})");
|
||||
|
||||
// User should ask metabot for a change
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("scenarios > models list view", () => {
|
||||
cy.log("Ensure List view is enabled");
|
||||
cy.findByTestId("list-view").should("be.visible");
|
||||
|
||||
cy.findByTestId("dataset-edit-bar").button("Save changes").click();
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
cy.log("Display data as list after saving");
|
||||
@@ -147,7 +147,7 @@ describe("scenarios > models list view", () => {
|
||||
cy.findByRole("img").should("have.attr", "aria-label", "company icon");
|
||||
});
|
||||
|
||||
cy.findByTestId("dataset-edit-bar").button("Save changes").click();
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
cy.log("Verify that custom column set is correct");
|
||||
@@ -252,7 +252,7 @@ describe("scenarios > models list view", () => {
|
||||
cy.findByText("List").click();
|
||||
});
|
||||
|
||||
cy.findByTestId("dataset-edit-bar").button("Save changes").click();
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
cy.findByTestId("list-view").should("be.visible");
|
||||
@@ -298,7 +298,7 @@ describe("scenarios > models list view", () => {
|
||||
cy.findByText("List").click();
|
||||
});
|
||||
|
||||
cy.findByTestId("dataset-edit-bar").button("Save changes").click();
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
cy.findByTestId("list-view").should("be.visible");
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("scenarios > models query editor", () => {
|
||||
});
|
||||
|
||||
cy.findByTestId("data-step-cell").contains("Orders");
|
||||
cy.button("Save changes").should("be.disabled");
|
||||
cy.button("Save").should("be.disabled");
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Row limit").click();
|
||||
@@ -49,7 +49,7 @@ describe("scenarios > models query editor", () => {
|
||||
.should("contain", "37.65")
|
||||
.and("not.contain", "109.22");
|
||||
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.url()
|
||||
@@ -141,7 +141,7 @@ describe("scenarios > models query editor", () => {
|
||||
});
|
||||
|
||||
cy.url().should("include", "/query");
|
||||
cy.button("Save changes").should("be.disabled");
|
||||
cy.button("Save").should("be.disabled");
|
||||
|
||||
H.NativeEditor.focus().type("{backspace}2");
|
||||
|
||||
@@ -151,7 +151,7 @@ describe("scenarios > models query editor", () => {
|
||||
.should("contain", "37.65")
|
||||
.and("not.contain", "109.22");
|
||||
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.get("[data-testid=cell-data]")
|
||||
@@ -182,7 +182,7 @@ describe("scenarios > models query editor", () => {
|
||||
});
|
||||
|
||||
cy.url().should("include", "/query");
|
||||
cy.button("Save changes").should("be.disabled");
|
||||
cy.button("Save").should("be.disabled");
|
||||
|
||||
H.NativeEditor.focus().type("{backspace}2");
|
||||
|
||||
@@ -236,7 +236,7 @@ describe("scenarios > models query editor", () => {
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText(/Syntax error in SQL/).should("not.exist");
|
||||
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.get("[data-testid=cell-data]").contains(1);
|
||||
|
||||
@@ -286,13 +286,11 @@ describe("issue 20517", () => {
|
||||
});
|
||||
|
||||
it("should be able to save metadata changes with empty description (metabase#20517)", () => {
|
||||
cy.findByTestId("dataset-edit-bar")
|
||||
.button("Save changes")
|
||||
.should("be.disabled");
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").should("be.disabled");
|
||||
cy.findByDisplayValue(/^This is a unique ID/).clear();
|
||||
cy.findByDisplayValue(/^This is a unique ID/).should("not.exist");
|
||||
cy.findByTestId("dataset-edit-bar")
|
||||
.button("Save changes")
|
||||
.button("Save")
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
cy.wait("@updateModel").then(({ response: { body, statusCode } }) => {
|
||||
@@ -336,7 +334,7 @@ describe("issue 20624", { tags: "@skip" }, () => {
|
||||
// Let's set a new name for it
|
||||
cy.findByDisplayValue(renamedColumn).clear().type("Foo").blur();
|
||||
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateCard");
|
||||
|
||||
cy.get("[data-testid=cell-data]").should("contain", "Foo");
|
||||
@@ -419,7 +417,7 @@ describe("issue 22517", () => {
|
||||
|
||||
renameColumn("ID", "Foo");
|
||||
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateMetadata");
|
||||
});
|
||||
|
||||
@@ -449,7 +447,7 @@ describe("issue 22517", () => {
|
||||
cy.findByText("Foo");
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Save changes").click();
|
||||
cy.findByText("Save").click();
|
||||
|
||||
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
|
||||
cy.findByText("Foo");
|
||||
@@ -482,7 +480,7 @@ describe("issue 22518", () => {
|
||||
cy.findByTestId("native-query-editor-container").icon("play").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
cy.findByTestId("dataset-edit-bar").button("Save changes").click();
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
|
||||
cy.findAllByTestId("header-cell")
|
||||
.should("have.length", 3)
|
||||
@@ -575,7 +573,7 @@ describe(
|
||||
// Make sure the column name updated before saving
|
||||
cy.findByDisplayValue("Created At");
|
||||
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateModel");
|
||||
|
||||
H.visitModel(id);
|
||||
@@ -730,7 +728,7 @@ describe("issue 23421", () => {
|
||||
H.NativeEditor.get().should("be.visible").and("contain", query);
|
||||
cy.findByRole("columnheader", { name: "id" }).should("be.visible");
|
||||
cy.findByRole("columnheader", { name: "created_at" }).should("be.visible");
|
||||
cy.button("Save changes").should("be.visible");
|
||||
cy.button("Save").should("be.visible");
|
||||
});
|
||||
|
||||
it("`visualization_settings` with hidden columns should not break UI (metabase#23421)", () => {
|
||||
@@ -745,7 +743,7 @@ describe("issue 23421", () => {
|
||||
cy.findByText("id").should("be.visible");
|
||||
cy.findByText("created_at").should("be.visible");
|
||||
});
|
||||
cy.button("Save changes").should("be.disabled");
|
||||
cy.button("Save").should("be.disabled");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -903,7 +901,7 @@ describe("issue 28193", () => {
|
||||
cy.findByTestId("run-button").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.location("pathname").should("not.include", "/query");
|
||||
|
||||
assertOnColumns();
|
||||
@@ -1048,7 +1046,7 @@ describe("issue 29517 - nested question based on native model with remapped valu
|
||||
mapModelColumnToDatabase({ table: "Orders", field: "Created At" });
|
||||
|
||||
cy.intercept("PUT", "/api/card/*").as("updateModel");
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateModel");
|
||||
|
||||
const nestedQuestionDetails = {
|
||||
@@ -1160,7 +1158,7 @@ describe("issue 53556 - nested question based on native model with remapped valu
|
||||
mapModelColumnToDatabase({ table: "Orders", field: "Total" });
|
||||
|
||||
cy.intercept("PUT", "/api/card/*").as("updateModel");
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateModel");
|
||||
|
||||
const nestedQuestionDetails = {
|
||||
@@ -1356,7 +1354,7 @@ FROM
|
||||
mapModelColumnToDatabase({ table: "People", field: "Source" });
|
||||
|
||||
cy.intercept("PUT", "/api/card/*").as("updateModel");
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateModel");
|
||||
|
||||
const nestedQuestionDetails = {
|
||||
@@ -1950,7 +1948,7 @@ describe("issues 35039 and 37009", () => {
|
||||
cy.wait("@dataset");
|
||||
|
||||
cy.findByTestId("dataset-edit-bar").within(() => {
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.button("Saving…").should("not.exist");
|
||||
});
|
||||
|
||||
@@ -2019,7 +2017,7 @@ describe("issue 37009", () => {
|
||||
H.popover().findByText("Edit query definition").click();
|
||||
H.NativeEditor.focus().type(" WHERE CATEGORY = 'Gadget'");
|
||||
cy.findByTestId("dataset-edit-bar")
|
||||
.button("Save changes")
|
||||
.button("Save")
|
||||
.should("be.disabled")
|
||||
.trigger("mousemove", { force: true });
|
||||
cy.findByRole("tooltip").should(
|
||||
@@ -2030,7 +2028,7 @@ describe("issue 37009", () => {
|
||||
cy.wait("@dataset");
|
||||
cy.findByRole("tooltip").should("not.exist");
|
||||
cy.findByTestId("dataset-edit-bar")
|
||||
.button("Save changes")
|
||||
.button("Save")
|
||||
.should("be.enabled")
|
||||
.click();
|
||||
cy.wait("@updateCard")
|
||||
@@ -2114,7 +2112,7 @@ describe("issue 40252", () => {
|
||||
cy.intercept("POST", "/api/dataset").as("dataset");
|
||||
|
||||
cy.findByTestId("dataset-edit-bar")
|
||||
.findByRole("button", { name: "Save changes" })
|
||||
.findByRole("button", { name: "Save" })
|
||||
.should("be.enabled")
|
||||
.click();
|
||||
|
||||
@@ -2167,7 +2165,7 @@ describe("issue 42355", () => {
|
||||
cy.findByText("Orders").click();
|
||||
cy.findByText("ID").click();
|
||||
});
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
|
||||
cy.log("check metadata changes are visible");
|
||||
H.openQuestionActions();
|
||||
@@ -2253,14 +2251,14 @@ describe("issue 45926", () => {
|
||||
cy.findByDisplayValue("ID").type(" updated");
|
||||
});
|
||||
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
cy.findByRole("columnheader", { name: "ID updated" }).should("be.visible");
|
||||
H.openQuestionActions("Edit query definition");
|
||||
cy.button("Sort").click();
|
||||
H.popover().findByText("ID").click();
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
H.questionInfoButton().click();
|
||||
|
||||
@@ -84,7 +84,7 @@ describe("issue 29943", () => {
|
||||
H.waitForLoaderToBeRemoved();
|
||||
|
||||
reorderTotalAndCustomColumns();
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@dataset");
|
||||
|
||||
H.openQuestionActions();
|
||||
@@ -245,7 +245,7 @@ describe("issue 23103", () => {
|
||||
cy.findByText("Category").click();
|
||||
});
|
||||
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateModel");
|
||||
cy.button("Saving…").should("not.exist");
|
||||
|
||||
@@ -734,7 +734,7 @@ describe("issue 39749", () => {
|
||||
cy.findByLabelText("Description").should("have.text", "A");
|
||||
H.tableHeaderClick("Sum of Total");
|
||||
cy.findByLabelText("Description").should("have.text", "B");
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateModel");
|
||||
|
||||
cy.log("verify that the description was updated successfully");
|
||||
@@ -853,7 +853,7 @@ describe("issue 33844", () => {
|
||||
cy.findAllByTestId("detail-shortcut").should("not.exist");
|
||||
H.tableHeaderClick("ID");
|
||||
cy.findByLabelText("Detail views only").click();
|
||||
cy.button(isNew ? "Save" : "Save changes").click();
|
||||
cy.button("Save").click();
|
||||
if (isNew) {
|
||||
H.modal().button("Save").click();
|
||||
cy.wait("@createModel");
|
||||
@@ -877,7 +877,7 @@ describe("issue 33844", () => {
|
||||
H.tableHeaderClick("ID");
|
||||
cy.findByLabelText("Detail views only").should("be.checked");
|
||||
cy.findByLabelText("Table and details views").click();
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateModel");
|
||||
cy.wait("@dataset");
|
||||
H.tableInteractive().findByText("ID").should("be.visible");
|
||||
@@ -933,7 +933,7 @@ describe("issue 45924", () => {
|
||||
cy.findByTestId("dataset-edit-bar").findByText("Columns").click();
|
||||
H.tableHeaderClick("ID1");
|
||||
cy.findByLabelText("Display name").should("have.value", "ID1");
|
||||
cy.findByTestId("dataset-edit-bar").button("Save changes").click();
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
cy.wait("@updateCard");
|
||||
cy.wait("@dataset");
|
||||
H.tableInteractive().findByText("ID1").should("be.visible");
|
||||
@@ -995,7 +995,7 @@ describe("issue 39993", () => {
|
||||
H.moveDnDKitElement(H.tableHeaderColumn(columnName), {
|
||||
horizontal: -100,
|
||||
});
|
||||
cy.button("Save changes").click();
|
||||
cy.button("Save").click();
|
||||
cy.wait("@updateModel");
|
||||
cy.findAllByTestId("header-cell").eq(0).should("have.text", "Exp");
|
||||
cy.findAllByTestId("header-cell").eq(1).should("have.text", "ID");
|
||||
@@ -1440,7 +1440,7 @@ describe("issue 20624", () => {
|
||||
H.waitForLoaderToBeRemoved();
|
||||
H.tableHeaderClick("Vendor");
|
||||
cy.findByLabelText("Display name").clear().type("Retailer");
|
||||
cy.button("Save changes").should("be.enabled").click();
|
||||
cy.button("Save").should("be.enabled").click();
|
||||
cy.wait("@updateCard");
|
||||
H.tableInteractive().within(() => {
|
||||
cy.findByText("Retailer").should("be.visible");
|
||||
@@ -1493,14 +1493,14 @@ describe("issue 32037", () => {
|
||||
|
||||
it("should show unsaved changes modal and allow to discard changes when editing model's query (metabase#32037)", () => {
|
||||
H.openQuestionActions("Edit query definition");
|
||||
cy.button("Save changes").should("be.disabled");
|
||||
cy.button("Save").should("be.disabled");
|
||||
H.filter({ mode: "notebook" });
|
||||
H.popover().within(() => {
|
||||
cy.findByText("ID").click();
|
||||
cy.findByPlaceholderText("Enter an ID").type("1").blur();
|
||||
cy.button("Add filter").click();
|
||||
});
|
||||
cy.button("Save changes").should("be.enabled");
|
||||
cy.button("Save").should("be.enabled");
|
||||
cy.go("back");
|
||||
|
||||
verifyDiscardingChanges();
|
||||
@@ -1509,9 +1509,9 @@ describe("issue 32037", () => {
|
||||
it("should show unsaved changes modal and allow to discard changes when editing model's metadata (metabase#32037)", () => {
|
||||
H.openQuestionActions("Edit metadata");
|
||||
H.waitForLoaderToBeRemoved();
|
||||
cy.button("Save changes").should("be.disabled");
|
||||
cy.button("Save").should("be.disabled");
|
||||
cy.findByLabelText("Description").type("123").blur();
|
||||
cy.button("Save changes").should("be.enabled");
|
||||
cy.button("Save").should("be.enabled");
|
||||
cy.go("back");
|
||||
|
||||
verifyDiscardingChanges();
|
||||
@@ -1524,7 +1524,7 @@ describe("issue 32037", () => {
|
||||
});
|
||||
|
||||
H.tableInteractive().should("be.visible");
|
||||
cy.button("Save changes").should("not.exist");
|
||||
cy.button("Save").should("not.exist");
|
||||
cy.get("@modelPathname").then((modelPathname) => {
|
||||
cy.location("pathname").should("eq", modelPathname);
|
||||
});
|
||||
|
||||
@@ -2664,10 +2664,9 @@ describe("issue 23449", () => {
|
||||
cy.button("Save").click();
|
||||
|
||||
cy.log("make a model on Reviews");
|
||||
cy.findByRole("link", { name: "Exit admin" }).click();
|
||||
H.navigationSidebar().findByLabelText("Browse models").click();
|
||||
H.benchNav().findByText("Models").click();
|
||||
cy.findByLabelText("Create a new model").click();
|
||||
cy.findByRole("link", { name: /Use the notebook editor/ }).click();
|
||||
H.popover().findByText("Query builder").click();
|
||||
H.entityPickerModal().findByText("Reviews").click();
|
||||
|
||||
cy.log("Remove Body column");
|
||||
@@ -2675,17 +2674,13 @@ describe("issue 23449", () => {
|
||||
H.popover().findByText("Body").click();
|
||||
|
||||
cy.log("save model");
|
||||
cy.findByTestId("dataset-edit-bar").button("Save").click();
|
||||
cy.findByTestId("bench-pane-header").button("Save").click();
|
||||
|
||||
cy.log("confirm save in a modal");
|
||||
H.modal().button("Save").click();
|
||||
|
||||
cy.log("check that the data renders");
|
||||
checkTable();
|
||||
|
||||
cy.log("reload to flush cached results and check again");
|
||||
cy.findByTestId("qb-header").button("Refresh").click();
|
||||
checkTable();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
(def ^:private GroupPermissionsGraph
|
||||
[:map-of
|
||||
[:enum :setting :monitoring :subscription]
|
||||
[:enum :setting :monitoring :subscription :transforms]
|
||||
[:enum :yes :no]])
|
||||
|
||||
(def ^:private ApplicationPermissionsGraph
|
||||
@@ -45,7 +45,8 @@
|
||||
[permission-set]
|
||||
{:setting (permission-for-type permission-set :setting)
|
||||
:monitoring (permission-for-type permission-set :monitoring)
|
||||
:subscription (permission-for-type permission-set :subscription)})
|
||||
:subscription (permission-for-type permission-set :subscription)
|
||||
:transforms (permission-for-type permission-set :transforms)})
|
||||
|
||||
(mu/defn graph :- ApplicationPermissionsGraph
|
||||
"Fetch a graph representing the application permissions status for groups that has at least one application permission
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
[metabase.api.util.handlers :as handlers]
|
||||
[metabase.driver.util :as driver.u]
|
||||
[metabase.events.core :as events]
|
||||
[metabase.models.interface :as mi]
|
||||
[metabase.permissions.core :as perms]
|
||||
[metabase.queries.schema :as queries.schema]
|
||||
[metabase.request.core :as request]
|
||||
[metabase.util :as u]
|
||||
@@ -88,7 +90,7 @@
|
||||
(defn get-transforms
|
||||
"Get a list of transforms."
|
||||
[& {:keys [last_run_start_time last_run_statuses tag_ids]}]
|
||||
(api/check-superuser)
|
||||
(api/check-403 (perms/current-user-has-application-permissions? :transforms))
|
||||
(let [transforms (t2/select :model/Transform)]
|
||||
(into []
|
||||
(comp (transforms.util/->date-field-filter-xf [:last_run :start_time] last_run_start_time)
|
||||
@@ -118,7 +120,7 @@
|
||||
[:target ::transform-target]
|
||||
[:run_trigger {:optional true} ::run-trigger]
|
||||
[:tag_ids {:optional true} [:sequential ms/PositiveInt]]]]
|
||||
(api/check-superuser)
|
||||
(api/check-403 (perms/current-user-has-application-permissions? :transforms))
|
||||
(check-database-feature body)
|
||||
(check-feature-enabled! body)
|
||||
(api/check (not (transforms.util/target-table-exists? body))
|
||||
@@ -139,8 +141,7 @@
|
||||
(defn get-transform
|
||||
"Get a specific transform."
|
||||
[id]
|
||||
(api/check-superuser)
|
||||
(let [{:keys [target] :as transform} (api/check-404 (t2/select-one :model/Transform id))
|
||||
(let [{:keys [target] :as transform} (api/check-404 (api/read-check (t2/select-one :model/Transform id)))
|
||||
target-table (transforms.util/target-table (transforms.i/target-db-id transform) target :active true)]
|
||||
(-> transform
|
||||
(t2/hydrate :last_run :transform_tag_ids)
|
||||
@@ -157,12 +158,11 @@
|
||||
"Get the dependencies of a specific transform."
|
||||
[{:keys [id]} :- [:map
|
||||
[:id ms/PositiveInt]]]
|
||||
(api/check-superuser)
|
||||
(let [id->transform (t2/select-pk->fn identity :model/Transform)
|
||||
_ (api/check-404 (get id->transform id))
|
||||
_ (api/check-404 (api/read-check (get id->transform id)))
|
||||
global-ordering (transforms.ordering/transform-ordering (vals id->transform))
|
||||
dep-ids (get global-ordering id)]
|
||||
(map id->transform dep-ids)))
|
||||
(filter mi/can-read? (map id->transform dep-ids))))
|
||||
|
||||
(api.macros/defendpoint :get "/run"
|
||||
"Get transform runs based on a set of filter params."
|
||||
@@ -195,10 +195,9 @@
|
||||
[:target {:optional true} ::transform-target]
|
||||
[:run_trigger {:optional true} ::run-trigger]
|
||||
[:tag_ids {:optional true} [:sequential ms/PositiveInt]]]]
|
||||
(api/check-superuser)
|
||||
(let [transform (t2/with-transaction [_]
|
||||
;; Cycle detection should occur within the transaction to avoid race
|
||||
(let [old (t2/select-one :model/Transform id)
|
||||
(let [old (api/write-check (t2/select-one :model/Transform id))
|
||||
new (merge old body)
|
||||
target-fields #(-> % :target (select-keys [:schema :name]))]
|
||||
;; we must validate on a full transform object
|
||||
@@ -224,7 +223,7 @@
|
||||
"Delete a transform."
|
||||
[{:keys [id]} :- [:map
|
||||
[:id ms/PositiveInt]]]
|
||||
(api/check-superuser)
|
||||
(api/write-check :model/Transform id)
|
||||
(t2/delete! :model/Transform id)
|
||||
nil)
|
||||
|
||||
@@ -232,7 +231,7 @@
|
||||
"Delete a transform's output table."
|
||||
[{:keys [id]} :- [:map
|
||||
[:id ms/PositiveInt]]]
|
||||
(api/check-superuser)
|
||||
(api/write-check :model/Transform id)
|
||||
(transforms.util/delete-target-table-by-id! id)
|
||||
nil)
|
||||
|
||||
@@ -240,7 +239,7 @@
|
||||
"Cancel the current run for a given transform."
|
||||
[{:keys [id]} :- [:map
|
||||
[:id ms/PositiveInt]]]
|
||||
(api/check-superuser)
|
||||
(api/write-check :model/Transform id)
|
||||
(let [transform (api/check-404 (t2/select-one :model/Transform id))
|
||||
run (api/check-404 (transform-run/running-run-for-transform-id id))]
|
||||
(transform-run-cancelation/mark-cancel-started-run! (:id run))
|
||||
@@ -252,7 +251,7 @@
|
||||
"Run a transform."
|
||||
[{:keys [id]} :- [:map
|
||||
[:id ms/PositiveInt]]]
|
||||
(api/check-superuser)
|
||||
(api/write-check :model/Transform id)
|
||||
(let [transform (api/check-404 (t2/select-one :model/Transform id))
|
||||
_ (check-feature-enabled! transform)
|
||||
start-promise (promise)]
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
[medley.core :as m]
|
||||
[metabase-enterprise.transforms.interface :as transforms.i]
|
||||
[metabase-enterprise.transforms.models.transform-run :as transform-run]
|
||||
[metabase.api.common :as api]
|
||||
[metabase.events.core :as events]
|
||||
[metabase.lib-be.core :as lib-be]
|
||||
[metabase.lib.core :as lib]
|
||||
[metabase.models.interface :as mi]
|
||||
[metabase.models.serialization :as serdes]
|
||||
[metabase.permissions.core :as perms]
|
||||
[metabase.search.core :as search.core]
|
||||
[metabase.search.ingestion :as search]
|
||||
[metabase.search.spec :as search.spec]
|
||||
@@ -23,10 +25,17 @@
|
||||
(doseq [trait [:metabase/model :hook/entity-id :hook/timestamped?]]
|
||||
(derive :model/Transform trait))
|
||||
|
||||
;; Only superusers can access transforms
|
||||
(doto :model/Transform
|
||||
(derive ::mi/read-policy.superuser)
|
||||
(derive ::mi/write-policy.superuser))
|
||||
(defmethod mi/can-read? :model/Transform
|
||||
([_instance]
|
||||
(perms/set-has-application-permission-of-type? @api/*current-user-permissions-set* :transforms))
|
||||
([_model _pk]
|
||||
(perms/set-has-application-permission-of-type? @api/*current-user-permissions-set* :transforms)))
|
||||
|
||||
(defmethod mi/can-write? :model/Transform
|
||||
([_instance]
|
||||
(perms/set-has-application-permission-of-type? @api/*current-user-permissions-set* :transforms))
|
||||
([_model _pk]
|
||||
(perms/set-has-application-permission-of-type? @api/*current-user-permissions-set* :transforms)))
|
||||
|
||||
(defn- transform-source-out [m]
|
||||
(-> m
|
||||
|
||||
@@ -46,10 +46,12 @@
|
||||
(with-new-group-and-current-graph group-id current-graph
|
||||
(let [new-graph (assoc-in current-graph [:groups group-id] {:setting :yes
|
||||
:monitoring :no
|
||||
:subscription :no})
|
||||
:subscription :no
|
||||
:transforms :no})
|
||||
_ (g-perms/update-graph! new-graph)
|
||||
updated-graph (g-perms/graph)]
|
||||
(is (partial= (:groups new-graph) (:groups updated-graph)))
|
||||
(is (= (:groups new-graph) (:groups updated-graph)))
|
||||
(is (=? (:groups new-graph) (:groups updated-graph)))
|
||||
(is (= (inc (:revision current-graph)) (:revision updated-graph)))
|
||||
(is (< initial-revision (a-perm-revision/latest-id)))))))
|
||||
|
||||
@@ -94,7 +96,8 @@
|
||||
(let [current-graph (g-perms/graph)
|
||||
new-graph (assoc-in current-graph [:groups group-id] {:setting :yes
|
||||
:subscription :yes
|
||||
:monitoring :no})
|
||||
:monitoring :no
|
||||
:transforms :no})
|
||||
_ (g-perms/update-graph! new-graph)
|
||||
updated-graph (g-perms/graph)]
|
||||
(is (= (:groups new-graph) (:groups updated-graph)))
|
||||
|
||||
@@ -1221,7 +1221,7 @@
|
||||
;; note: t2 not included because it's a (non-native) MBQL query
|
||||
(mt/obj->json->obj (select-keys t3 [:id :entity_id :name :description :source]))]
|
||||
:conversation_id conversation-id}
|
||||
(-> (mt/user-http-request :rasta :post 200 "ee/metabot-tools/get-transforms"
|
||||
(-> (mt/user-http-request :crowberto :post 200 "ee/metabot-tools/get-transforms"
|
||||
{:request-options {:headers {"x-metabase-session" crowberto-ai-token}}}
|
||||
{:conversation_id conversation-id})
|
||||
(update :structured_output (fn [output]
|
||||
@@ -1255,7 +1255,7 @@
|
||||
:conversation_id conversation-id}))))
|
||||
(testing "With non-existent transform"
|
||||
(is (= "Not found."
|
||||
(mt/user-http-request :rasta :post 404 "ee/metabot-tools/get-transform-details"
|
||||
(mt/user-http-request :crowberto :post 404 "ee/metabot-tools/get-transform-details"
|
||||
{:request-options {:headers {"x-metabase-session" crowberto-ai-token}}}
|
||||
{:arguments {:transform_id (+ 10000 (:id t2))}
|
||||
:conversation_id conversation-id}))))
|
||||
@@ -1264,7 +1264,7 @@
|
||||
(testing (:name transform)
|
||||
(is (=? {:structured_output (mt/obj->json->obj (select-keys transform [:id :entity_id :name :description :source :target]))
|
||||
:conversation_id conversation-id}
|
||||
(mt/user-http-request :rasta :post 200 "ee/metabot-tools/get-transform-details"
|
||||
(mt/user-http-request :crowberto :post 200 "ee/metabot-tools/get-transform-details"
|
||||
{:request-options {:headers {"x-metabase-session" crowberto-ai-token}}}
|
||||
{:arguments {:transform_id (:id transform)}
|
||||
:conversation_id conversation-id}))))))))))
|
||||
|
||||
@@ -132,8 +132,11 @@ export const transformJobApi = EnterpriseApi.injectEndpoints({
|
||||
method: "DELETE",
|
||||
url: `/api/ee/transform-job/${id}`,
|
||||
}),
|
||||
invalidatesTags: (_, error) =>
|
||||
invalidateTags(error, [listTag("transform-job")]),
|
||||
invalidatesTags: (_, error, id) =>
|
||||
invalidateTags(error, [
|
||||
listTag("transform-job"),
|
||||
idTag("transform-job", id),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -134,7 +134,7 @@ function getItemIcon(item: DependencyItem): IconName {
|
||||
}
|
||||
|
||||
if (item.type === "transform") {
|
||||
return "refresh_downstream";
|
||||
return "transform";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
|
||||
@@ -188,7 +188,7 @@ describe("CheckDependenciesForm", () => {
|
||||
});
|
||||
expect(screen.getByRole("link", { name: /SQL transform/ })).toHaveAttribute(
|
||||
"href",
|
||||
"/admin/transforms/1",
|
||||
"/bench/transforms/1",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { DependencyEntry } from "metabase-types/api";
|
||||
import S from "./DependencyGraph.module.css";
|
||||
import { GraphContext } from "./GraphContext";
|
||||
import { GraphDependencyPanel } from "./GraphDependencyPanel";
|
||||
import { GraphEdge } from "./GraphEdge";
|
||||
import { GraphEntryInput } from "./GraphEntryInput";
|
||||
import { GraphInfoPanel } from "./GraphInfoPanel";
|
||||
import { GraphNode } from "./GraphNode";
|
||||
@@ -32,11 +33,21 @@ const NODE_TYPES = {
|
||||
node: GraphNode,
|
||||
};
|
||||
|
||||
type DependencyGraphProps = {
|
||||
entry?: DependencyEntry;
|
||||
const EDGE_TYPES = {
|
||||
edge: GraphEdge,
|
||||
};
|
||||
|
||||
export function DependencyGraph({ entry }: DependencyGraphProps) {
|
||||
type DependencyGraphProps = {
|
||||
entry?: DependencyEntry;
|
||||
getGraphUrl: (entry?: DependencyEntry) => string;
|
||||
withEntryPicker?: boolean;
|
||||
};
|
||||
|
||||
export function DependencyGraph({
|
||||
entry,
|
||||
getGraphUrl,
|
||||
withEntryPicker,
|
||||
}: DependencyGraphProps) {
|
||||
const {
|
||||
data: graph,
|
||||
isFetching,
|
||||
@@ -48,13 +59,11 @@ export function DependencyGraph({ entry }: DependencyGraphProps) {
|
||||
const { sendErrorToast } = useMetadataToasts();
|
||||
|
||||
const entryNode = useMemo(() => {
|
||||
return entry != null ? findNode(nodes, entry.id, entry.type) : null;
|
||||
return entry != null ? findNode(nodes, entry) : null;
|
||||
}, [nodes, entry]);
|
||||
|
||||
const selectedNode = useMemo(() => {
|
||||
return selection != null
|
||||
? findNode(nodes, selection.id, selection.type)
|
||||
: null;
|
||||
return selection != null ? findNode(nodes, selection) : null;
|
||||
}, [nodes, selection]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -87,6 +96,7 @@ export function DependencyGraph({ entry }: DependencyGraphProps) {
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={NODE_TYPES}
|
||||
edgeTypes={EDGE_TYPES}
|
||||
fitView
|
||||
minZoom={MIN_ZOOM}
|
||||
maxZoom={MAX_ZOOM}
|
||||
@@ -99,24 +109,29 @@ export function DependencyGraph({ entry }: DependencyGraphProps) {
|
||||
<GraphNodeLayout />
|
||||
<Panel position="top-left">
|
||||
<Group>
|
||||
<GraphEntryInput
|
||||
node={entryNode?.data ?? null}
|
||||
isGraphFetching={isFetching}
|
||||
/>
|
||||
{withEntryPicker && (
|
||||
<GraphEntryInput
|
||||
node={entryNode?.data ?? null}
|
||||
isGraphFetching={isFetching}
|
||||
getGraphUrl={getGraphUrl}
|
||||
/>
|
||||
)}
|
||||
{nodes.length > 1 && <GraphSelectInput nodes={nodes} />}
|
||||
</Group>
|
||||
</Panel>
|
||||
{selection != null && selection.withInfo && selectedNode != null && (
|
||||
{selection != null && selectedNode != null && (
|
||||
<Panel className={S.panel} position="top-right">
|
||||
{selection.groupType != null ? (
|
||||
<GraphDependencyPanel
|
||||
node={selectedNode.data}
|
||||
groupType={selection.groupType}
|
||||
getGraphUrl={getGraphUrl}
|
||||
onClose={handlePanelClose}
|
||||
/>
|
||||
) : (
|
||||
<GraphInfoPanel
|
||||
node={selectedNode.data}
|
||||
getGraphUrl={getGraphUrl}
|
||||
onClose={handlePanelClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,11 @@ import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErr
|
||||
import { SEARCH_DEBOUNCE_DURATION } from "metabase/lib/constants";
|
||||
import { Box, Card } from "metabase/ui";
|
||||
import { useListNodeDependentsQuery } from "metabase-enterprise/api";
|
||||
import type { DependencyGroupType, DependencyNode } from "metabase-types/api";
|
||||
import type {
|
||||
DependencyEntry,
|
||||
DependencyGroupType,
|
||||
DependencyNode,
|
||||
} from "metabase-types/api";
|
||||
|
||||
import { DEPENDENTS_SEARCH_THRESHOLD } from "../constants";
|
||||
|
||||
@@ -25,12 +29,14 @@ import {
|
||||
type GraphDependencyPanelProps = {
|
||||
node: DependencyNode;
|
||||
groupType: DependencyGroupType;
|
||||
getGraphUrl: (entry: DependencyEntry) => string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function GraphDependencyPanel({
|
||||
node,
|
||||
groupType,
|
||||
getGraphUrl,
|
||||
onClose,
|
||||
}: GraphDependencyPanelProps) {
|
||||
const {
|
||||
@@ -83,7 +89,7 @@ export function GraphDependencyPanel({
|
||||
{t`Didn't find any results`}
|
||||
</Box>
|
||||
) : (
|
||||
<PanelBody nodes={visibleNodes} />
|
||||
<PanelBody nodes={visibleNodes} getGraphUrl={getGraphUrl} />
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { memo } from "react";
|
||||
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
import { Box, Group, Stack } from "metabase/ui";
|
||||
import type { DependencyNode } from "metabase-types/api";
|
||||
import type { DependencyEntry, DependencyNode } from "metabase-types/api";
|
||||
|
||||
import { GraphBreadcrumbs } from "../../GraphBreadcrumbs";
|
||||
import { GraphExternalLink } from "../../GraphExternalLink";
|
||||
@@ -21,13 +20,21 @@ import { getNodeViewCountLabel } from "./utils";
|
||||
|
||||
type PanelBodyProps = {
|
||||
nodes: DependencyNode[];
|
||||
getGraphUrl: (entry: DependencyEntry) => string;
|
||||
};
|
||||
|
||||
export const PanelBody = memo(function ListBody({ nodes }: PanelBodyProps) {
|
||||
export const PanelBody = memo(function ListBody({
|
||||
nodes,
|
||||
getGraphUrl,
|
||||
}: PanelBodyProps) {
|
||||
return (
|
||||
<Box className={S.body}>
|
||||
{nodes.map((node) => (
|
||||
<ListItem key={getNodeId(node.id, node.type)} node={node} />
|
||||
<ListItem
|
||||
key={getNodeId(node.id, node.type)}
|
||||
node={node}
|
||||
getGraphUrl={getGraphUrl}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
@@ -35,9 +42,10 @@ export const PanelBody = memo(function ListBody({ nodes }: PanelBodyProps) {
|
||||
|
||||
type ListItemProps = {
|
||||
node: DependencyNode;
|
||||
getGraphUrl: (entry: DependencyEntry) => string;
|
||||
};
|
||||
|
||||
function ListItem({ node }: ListItemProps) {
|
||||
function ListItem({ node, getGraphUrl }: ListItemProps) {
|
||||
const label = getNodeLabel(node);
|
||||
const link = getNodeLink(node);
|
||||
const location = getNodeLocationInfo(node);
|
||||
@@ -49,7 +57,7 @@ function ListItem({ node }: ListItemProps) {
|
||||
<GraphLink
|
||||
label={label}
|
||||
icon={getNodeIcon(node)}
|
||||
url={Urls.dependencyGraph({ entry: node })}
|
||||
url={getGraphUrl(node)}
|
||||
/>
|
||||
{viewCount != null ? (
|
||||
<Box c="text-secondary" fz="sm" lh="1rem">
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { BezierEdge, type EdgeProps, useNodesInitialized } from "@xyflow/react";
|
||||
|
||||
export function GraphEdge({ style, ...props }: EdgeProps) {
|
||||
const isInitialized = useNodesInitialized();
|
||||
|
||||
return (
|
||||
<BezierEdge
|
||||
{...props}
|
||||
style={{
|
||||
...style,
|
||||
visibility: !isInitialized ? "hidden" : undefined,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./GraphEdge";
|
||||
@@ -8,7 +8,7 @@ import { getNodeIcon, getNodeLabel } from "../../utils";
|
||||
|
||||
type EntryButtonProps = {
|
||||
node: DependencyNode | null;
|
||||
onEntryChange: (entry: DependencyEntry | null) => void;
|
||||
onEntryChange: (entry: DependencyEntry | undefined) => void;
|
||||
onPickerOpen: () => void;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ export function EntryButton({
|
||||
}: EntryButtonProps) {
|
||||
const handleIconClick = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
onEntryChange(null);
|
||||
onEntryChange(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -106,7 +106,7 @@ export function EntryPickerModal({
|
||||
displayName: t`Transforms`,
|
||||
models: ["transform"],
|
||||
folderModels: [],
|
||||
icon: "refresh_downstream",
|
||||
icon: "transform",
|
||||
render: ({ onItemSelect }) => (
|
||||
<PLUGIN_TRANSFORMS.TransformPicker
|
||||
value={value ? getTransformPickerItem(value) : undefined}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useState } from "react";
|
||||
import { push } from "react-router-redux";
|
||||
|
||||
import { useDispatch } from "metabase/lib/redux";
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
import { Card } from "metabase/ui";
|
||||
import type {
|
||||
DependencyEntry,
|
||||
@@ -19,11 +18,13 @@ import { SEARCH_MODELS } from "./constants";
|
||||
type GraphEntryInputProps = {
|
||||
node: DependencyNode | null;
|
||||
isGraphFetching: boolean;
|
||||
getGraphUrl: (entry: DependencyEntry | undefined) => string;
|
||||
};
|
||||
|
||||
export function GraphEntryInput({
|
||||
node,
|
||||
isGraphFetching,
|
||||
getGraphUrl,
|
||||
}: GraphEntryInputProps) {
|
||||
const [searchModels, setSearchModels] =
|
||||
useState<SearchModel[]>(SEARCH_MODELS);
|
||||
@@ -31,8 +32,8 @@ export function GraphEntryInput({
|
||||
const [isPickerOpened, { open: openPicker, close: closePicker }] =
|
||||
useDisclosure();
|
||||
|
||||
const handleEntryChange = (newEntry: DependencyEntry | null) => {
|
||||
dispatch(push(Urls.dependencyGraph(newEntry ? { entry: newEntry } : {})));
|
||||
const handleEntryChange = (newEntry: DependencyEntry | undefined) => {
|
||||
dispatch(push(getGraphUrl(newEntry)));
|
||||
};
|
||||
|
||||
const handlePickerChange = (newEntry: DependencyEntry) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card } from "metabase/ui";
|
||||
import type { DependencyNode } from "metabase-types/api";
|
||||
import type { DependencyEntry, DependencyNode } from "metabase-types/api";
|
||||
|
||||
import S from "./GraphInfoPanel.module.css";
|
||||
import { PanelBody } from "./PanelBody";
|
||||
@@ -7,14 +7,19 @@ import { PanelHeader } from "./PanelHeader";
|
||||
|
||||
type GraphInfoPanelProps = {
|
||||
node: DependencyNode;
|
||||
getGraphUrl: (entry: DependencyEntry) => string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function GraphInfoPanel({ node, onClose }: GraphInfoPanelProps) {
|
||||
export function GraphInfoPanel({
|
||||
node,
|
||||
getGraphUrl,
|
||||
onClose,
|
||||
}: GraphInfoPanelProps) {
|
||||
return (
|
||||
<Card className={S.root} withBorder data-testid="graph-info-panel">
|
||||
<PanelHeader node={node} onClose={onClose} />
|
||||
<PanelBody node={node} />
|
||||
<PanelBody node={node} getGraphUrl={getGraphUrl} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getColumnIcon } from "metabase/common/utils/columns";
|
||||
import { getUserName } from "metabase/lib/user";
|
||||
import { Box, FixedSizeIcon, Group, Stack, Title } from "metabase/ui";
|
||||
import * as Lib from "metabase-lib";
|
||||
import type { DependencyNode } from "metabase-types/api";
|
||||
import type { DependencyEntry, DependencyNode } from "metabase-types/api";
|
||||
|
||||
import { GraphBreadcrumbs } from "../../GraphBreadcrumbs";
|
||||
import { GraphExternalLink } from "../../GraphExternalLink";
|
||||
@@ -25,14 +25,15 @@ import {
|
||||
|
||||
type PanelBodyProps = {
|
||||
node: DependencyNode;
|
||||
getGraphUrl: (entry: DependencyEntry) => string;
|
||||
};
|
||||
|
||||
export function PanelBody({ node }: PanelBodyProps) {
|
||||
export function PanelBody({ node, getGraphUrl }: PanelBodyProps) {
|
||||
return (
|
||||
<Stack className={S.body} p="lg" gap="lg">
|
||||
<DescriptionSection node={node} />
|
||||
<CreatorAndLastEditorSection node={node} />
|
||||
<TableSection node={node} />
|
||||
<TableSection node={node} getGraphUrl={getGraphUrl} />
|
||||
<FieldsSection node={node} />
|
||||
</Stack>
|
||||
);
|
||||
@@ -98,8 +99,13 @@ function CreatorAndLastEditorSection({ node }: SectionProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function TableSection({ node }: SectionProps) {
|
||||
const info = getNodeTableInfo(node);
|
||||
type TableSectionProps = {
|
||||
node: DependencyNode;
|
||||
getGraphUrl: (entry: DependencyEntry) => string;
|
||||
};
|
||||
|
||||
function TableSection({ node, getGraphUrl }: TableSectionProps) {
|
||||
const info = getNodeTableInfo(node, getGraphUrl);
|
||||
if (info == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { msgid, ngettext, t } from "ttag";
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
import type {
|
||||
CardCreatorInfo,
|
||||
DependencyEntry,
|
||||
DependencyNode,
|
||||
Field,
|
||||
LastEditInfo,
|
||||
@@ -66,7 +67,10 @@ export function getNodeLastEditedBy(node: DependencyNode): LastEditInfo | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function getNodeTableInfo(node: DependencyNode): NodeTableInfo | null {
|
||||
export function getNodeTableInfo(
|
||||
node: DependencyNode,
|
||||
getGraphUrl: (entry: DependencyEntry) => string,
|
||||
): NodeTableInfo | null {
|
||||
if (node.type !== "transform" && node.type !== "sandbox") {
|
||||
return null;
|
||||
}
|
||||
@@ -80,7 +84,7 @@ export function getNodeTableInfo(node: DependencyNode): NodeTableInfo | null {
|
||||
label: node.type === "transform" ? t`Generated table` : t`Restricted table`,
|
||||
title: {
|
||||
label: table.display_name,
|
||||
url: Urls.dependencyGraph({ entry: { id: table.id, type: "table" } }),
|
||||
url: getGraphUrl({ id: table.id, type: "table" }),
|
||||
},
|
||||
metadata: {
|
||||
label: t`View metadata`,
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card,
|
||||
.handle {
|
||||
visibility: hidden;
|
||||
|
||||
&.initialized {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.pill {
|
||||
color: var(--mb-color-text-primary);
|
||||
background-color: var(--mb-color-bg-medium);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type NodeProps,
|
||||
Position,
|
||||
useNodeConnections,
|
||||
useNodesInitialized,
|
||||
} from "@xyflow/react";
|
||||
import cx from "classnames";
|
||||
import { type MouseEvent, memo, useContext } from "react";
|
||||
@@ -46,17 +47,20 @@ export const GraphNode = memo(function ItemNode({
|
||||
const groups = getDependentGroups(node);
|
||||
const sources = useNodeConnections({ handleType: "source" });
|
||||
const targets = useNodeConnections({ handleType: "target" });
|
||||
const isSelected =
|
||||
selection != null && isSameNode(node, selection.id, selection.type);
|
||||
const isSelected = selection != null && isSameNode(node, selection);
|
||||
const isInitialized = useNodesInitialized();
|
||||
|
||||
const handleClick = () => {
|
||||
setSelection({ id: node.id, type: node.type, withInfo: true });
|
||||
setSelection({ id: node.id, type: node.type });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
className={cx(S.card, { [S.selected]: isSelected })}
|
||||
className={cx(S.card, {
|
||||
[S.initialized]: isInitialized,
|
||||
[S.selected]: isSelected,
|
||||
})}
|
||||
p="lg"
|
||||
withBorder
|
||||
aria-label={label}
|
||||
@@ -91,10 +95,20 @@ export const GraphNode = memo(function ItemNode({
|
||||
</Stack>
|
||||
</Card>
|
||||
{sources.length > 0 && (
|
||||
<Handle type="source" position={Position.Left} isConnectable={false} />
|
||||
<Handle
|
||||
className={cx(S.handle, { [S.initialized]: isInitialized })}
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
isConnectable={false}
|
||||
/>
|
||||
)}
|
||||
{targets.length > 0 && (
|
||||
<Handle type="target" position={Position.Right} isConnectable={false} />
|
||||
<Handle
|
||||
className={cx(S.handle, { [S.initialized]: isInitialized })}
|
||||
type="target"
|
||||
position={Position.Right}
|
||||
isConnectable={false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -115,7 +129,7 @@ function DependencyGroupButton({
|
||||
}: DependencyGroupButtonProps) {
|
||||
const isSelected =
|
||||
selection != null &&
|
||||
isSameNode(node, selection.id, selection.type) &&
|
||||
isSameNode(node, selection) &&
|
||||
selection.groupType === group.type;
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
@@ -124,7 +138,6 @@ function DependencyGroupButton({
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
groupType: group.type,
|
||||
withInfo: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -36,15 +36,15 @@ export function getDependencyGroupTitle(
|
||||
node: DependencyNode,
|
||||
groups: DependentGroup[],
|
||||
) {
|
||||
if (node.type === "transform") {
|
||||
return t`Generates`;
|
||||
}
|
||||
if (node.type === "sandbox") {
|
||||
return t`Restricts table data`;
|
||||
}
|
||||
if (groups.length === 0) {
|
||||
return t`Nothing uses this`;
|
||||
}
|
||||
if (node.type === "transform") {
|
||||
return t`Generates`;
|
||||
}
|
||||
return t`Used by`;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ export type GraphSelection = {
|
||||
id: DependencyId;
|
||||
type: DependencyType;
|
||||
groupType?: DependencyGroupType;
|
||||
withInfo?: boolean;
|
||||
};
|
||||
|
||||
export type GraphContextType = {
|
||||
|
||||
@@ -7,6 +7,7 @@ import visualizations from "metabase/visualizations";
|
||||
import type {
|
||||
CardType,
|
||||
DependencyEdge,
|
||||
DependencyEntry,
|
||||
DependencyGraph,
|
||||
DependencyGroupType,
|
||||
DependencyId,
|
||||
@@ -54,6 +55,7 @@ function getEdges(edges: DependencyEdge[]): Edge[] {
|
||||
|
||||
return {
|
||||
id: getEdgeId(sourceId, targetId),
|
||||
type: "edge",
|
||||
data: edge,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
@@ -71,19 +73,17 @@ export function getInitialGraph({ nodes, edges }: DependencyGraph): GraphData {
|
||||
}
|
||||
|
||||
export function isSameNode(
|
||||
node: DependencyNode,
|
||||
id: DependencyId,
|
||||
type: DependencyType,
|
||||
entry1: DependencyEntry,
|
||||
entry2: DependencyEntry,
|
||||
): boolean {
|
||||
return node.id === id && node.type === type;
|
||||
return entry1.id === entry2.id && entry1.type === entry2.type;
|
||||
}
|
||||
|
||||
export function findNode(
|
||||
nodes: NodeType[],
|
||||
id: DependencyId,
|
||||
type: DependencyType,
|
||||
): NodeType | null {
|
||||
return nodes.find((node) => isSameNode(node.data, id, type)) ?? null;
|
||||
entry: DependencyEntry,
|
||||
): NodeType | undefined {
|
||||
return nodes.find((node) => isSameNode(node.data, entry));
|
||||
}
|
||||
|
||||
export function getNodeLabel(node: DependencyNode): string {
|
||||
@@ -137,7 +137,7 @@ export function getNodeIconWithType(
|
||||
case "table":
|
||||
return "table";
|
||||
case "transform":
|
||||
return "refresh_downstream";
|
||||
return "transform";
|
||||
case "snippet":
|
||||
return "sql";
|
||||
case "dashboard":
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("getNodeIcon", () => {
|
||||
},
|
||||
{
|
||||
node: createMockTransformDependencyNode(),
|
||||
expectedIcon: "refresh_downstream",
|
||||
expectedIcon: "transform",
|
||||
},
|
||||
{
|
||||
node: createMockSnippetDependencyNode(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { CheckDependenciesResponse } from "metabase-types/api";
|
||||
type UseCheckDependenciesQueryData = {
|
||||
data?: CheckDependenciesResponse;
|
||||
error?: unknown;
|
||||
isLoading: boolean;
|
||||
isFetching?: boolean;
|
||||
};
|
||||
|
||||
type UseCheckDependenciesArgsData<TRequest> = {
|
||||
@@ -35,7 +35,7 @@ export function useCheckDependencies<TChange, TRequest>({
|
||||
>): UseCheckDependenciesResult<TChange> {
|
||||
const [change, setChange] = useState<TChange | null>(null);
|
||||
const [isConfirmationShown, setIsConfirmationShown] = useState(false);
|
||||
const [checkDependencies, { data, isLoading }] =
|
||||
const [checkDependencies, { data, isFetching = false }] =
|
||||
useLazyCheckDependenciesQuery();
|
||||
|
||||
const handleInitialSave = useCallback(
|
||||
@@ -70,7 +70,7 @@ export function useCheckDependencies<TChange, TRequest>({
|
||||
|
||||
return {
|
||||
checkData: data,
|
||||
isCheckingDependencies: isLoading,
|
||||
isCheckingDependencies: isFetching,
|
||||
isConfirmationShown,
|
||||
handleInitialSave,
|
||||
handleSaveAfterConfirmation,
|
||||
|
||||
@@ -8,9 +8,12 @@ import { useCheckCardDependencies } from "./hooks/use-check-card-dependencies";
|
||||
import { useCheckSnippetDependencies } from "./hooks/use-check-snippet-dependencies";
|
||||
import { useCheckTransformDependencies } from "./hooks/use-check-transform-dependencies";
|
||||
import { DependencyGraphPage } from "./pages/DependencyGraphPage";
|
||||
import { getBenchNavItems, getBenchRoutes } from "./routes";
|
||||
|
||||
if (hasPremiumFeature("dependencies")) {
|
||||
PLUGIN_DEPENDENCIES.isEnabled = true;
|
||||
PLUGIN_DEPENDENCIES.getBenchRoutes = getBenchRoutes;
|
||||
PLUGIN_DEPENDENCIES.getBenchNavItems = getBenchNavItems;
|
||||
PLUGIN_DEPENDENCIES.DependencyGraphPage = DependencyGraphPage;
|
||||
PLUGIN_DEPENDENCIES.CheckDependenciesForm = CheckDependenciesForm;
|
||||
PLUGIN_DEPENDENCIES.CheckDependenciesModal = CheckDependenciesModal;
|
||||
|
||||
@@ -1,13 +1,36 @@
|
||||
import type { Location } from "history";
|
||||
import { useContext } from "react";
|
||||
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
import { PLUGIN_DEPENDENCIES } from "metabase/plugins";
|
||||
|
||||
import { DependencyGraph } from "../../components/DependencyGraph";
|
||||
import type { DependencyGraphRawParams } from "../../types";
|
||||
import { isSameNode } from "../../components/DependencyGraph/utils";
|
||||
|
||||
import { parseParams } from "./utils";
|
||||
import { parseDependencyEntry } from "./utils";
|
||||
|
||||
type DependencyGraphPageProps = {
|
||||
params?: DependencyGraphRawParams;
|
||||
export type DependencyGraphPageQuery = {
|
||||
id?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export function DependencyGraphPage({ params }: DependencyGraphPageProps) {
|
||||
const { entry } = parseParams(params);
|
||||
return <DependencyGraph entry={entry} />;
|
||||
type DependencyGraphPageProps = {
|
||||
location?: Location<DependencyGraphPageQuery>;
|
||||
};
|
||||
|
||||
export function DependencyGraphPage({ location }: DependencyGraphPageProps) {
|
||||
const entry = parseDependencyEntry(location?.query?.id, location?.query.type);
|
||||
const { defaultEntry, baseUrl } = useContext(
|
||||
PLUGIN_DEPENDENCIES.DependencyGraphPageContext,
|
||||
);
|
||||
const withEntryPicker =
|
||||
defaultEntry == null || (entry != null && !isSameNode(entry, defaultEntry));
|
||||
|
||||
return (
|
||||
<DependencyGraph
|
||||
entry={entry ?? defaultEntry}
|
||||
getGraphUrl={(entry) => Urls.dependencyGraph({ entry, baseUrl })}
|
||||
withEntryPicker={withEntryPicker}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
import type { DependencyType } from "metabase-types/api";
|
||||
import type { DependencyEntry, DependencyType } from "metabase-types/api";
|
||||
|
||||
import type { DependencyGraphRawParams } from "../../types";
|
||||
|
||||
export function parseParams(
|
||||
params: DependencyGraphRawParams = {},
|
||||
): Urls.DependencyGraphParams {
|
||||
const id = Urls.extractEntityId(params.id);
|
||||
const type = parseDependencyType(params.type);
|
||||
return {
|
||||
entry: id != null && type != null ? { id, type } : undefined,
|
||||
};
|
||||
export function parseDependencyEntry(
|
||||
rawId?: string,
|
||||
rawType?: string,
|
||||
): DependencyEntry | undefined {
|
||||
const id = Urls.extractEntityId(rawId);
|
||||
const type = parseDependencyType(rawType);
|
||||
return id != null && type != null ? { id, type } : undefined;
|
||||
}
|
||||
|
||||
function parseDependencyType(type: unknown): DependencyType | null {
|
||||
function parseDependencyType(type: unknown): DependencyType | undefined {
|
||||
switch (type) {
|
||||
case "card":
|
||||
case "table":
|
||||
@@ -24,6 +21,6 @@ function parseDependencyType(type: unknown): DependencyType | null {
|
||||
case "sandbox":
|
||||
return type;
|
||||
default:
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
import type { BenchNavItem } from "metabase/bench/constants/navigation";
|
||||
import { Route } from "metabase/hoc/Title";
|
||||
|
||||
import { DependencyGraphPage } from "./pages/DependencyGraphPage";
|
||||
|
||||
export function getBenchRoutes(): ReactNode {
|
||||
return (
|
||||
<Route
|
||||
title={t`Dependency graph`}
|
||||
path="dependencies"
|
||||
component={DependencyGraphPage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const getBenchNavItems = (): BenchNavItem[] => {
|
||||
return [
|
||||
{
|
||||
id: "dependencies",
|
||||
url: "/bench/dependencies",
|
||||
icon: "network",
|
||||
getLabel: () => t`Dependency graph`,
|
||||
getDescription: () =>
|
||||
t`Use the Dependency Graph to see what's upstream and downstream of anything.`,
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
export type DependencyGraphRawParams = {
|
||||
id?: string;
|
||||
type?: string;
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export const Transforms = createEntity({
|
||||
objectSelectors: {
|
||||
getName: (transform: Transform) => transform && transform.name,
|
||||
getUrl: (transform: Transform) => transform && Urls.transform(transform.id),
|
||||
getIcon: () => ({ name: "refresh_downstream" }),
|
||||
getIcon: () => ({ name: "transform" }),
|
||||
getColor: () => color("brand"),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -17,15 +17,16 @@ export interface MetabotConfig {
|
||||
hideSuggestedPrompts?: boolean;
|
||||
preventClose?: boolean;
|
||||
preventRetryMessage?: boolean;
|
||||
suggestionModels: SuggestionModel[];
|
||||
suggestionModels?: SuggestionModel[];
|
||||
}
|
||||
|
||||
export interface MetabotProps {
|
||||
hide?: boolean;
|
||||
config?: MetabotConfig;
|
||||
w?: string;
|
||||
}
|
||||
|
||||
export const MetabotAuthenticated = ({ hide, config }: MetabotProps) => {
|
||||
export const MetabotAuthenticated = ({ hide, config, w }: MetabotProps) => {
|
||||
const { visible, setVisible } = useMetabotAgent();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,7 +56,7 @@ export const MetabotAuthenticated = ({ hide, config }: MetabotProps) => {
|
||||
|
||||
return (
|
||||
<ErrorBoundary errorComponent={() => null}>
|
||||
<MetabotChat config={config} />
|
||||
<MetabotChat config={config} w={w} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useLocation } from "react-use";
|
||||
import { t } from "ttag";
|
||||
|
||||
import { METAKEY } from "metabase/lib/browser";
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
import { AdminActionButton } from "metabase/nav/components/AdminNavbar/AdminNavbar.styled";
|
||||
import { Icon, Tooltip } from "metabase/ui";
|
||||
|
||||
import { trackMetabotChatOpened } from "../analytics";
|
||||
import { useMetabotAgent } from "../hooks";
|
||||
|
||||
export const MetabotAdminAppBarButton = () => {
|
||||
const metabot = useMetabotAgent();
|
||||
|
||||
const location = useLocation();
|
||||
const disabled = !location.pathname?.startsWith(Urls.transformList());
|
||||
|
||||
const handleClick = () => {
|
||||
if (!metabot.visible) {
|
||||
trackMetabotChatOpened("header");
|
||||
}
|
||||
|
||||
metabot.setVisible(!metabot.visible);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={
|
||||
disabled
|
||||
? `Metabot can't be viewed on this page`
|
||||
: t`Chat with Metabot (${METAKEY}+E)`
|
||||
}
|
||||
>
|
||||
<AdminActionButton disabled={disabled} onClick={handleClick}>
|
||||
<Icon name="metabot" />
|
||||
</AdminActionButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -140,7 +140,7 @@ export const AgentSuggestionMessage = ({
|
||||
<Paper
|
||||
shadow="none"
|
||||
radius="md"
|
||||
bg="white"
|
||||
bg="bg-white"
|
||||
className={S.container}
|
||||
data-testid="metabot-chat-suggestion"
|
||||
>
|
||||
@@ -152,7 +152,7 @@ export const AgentSuggestionMessage = ({
|
||||
className={cx(opened && S.headerOpened)}
|
||||
>
|
||||
<Flex align="center" gap="sm">
|
||||
<Icon name="refresh_downstream" size="1rem" c="text-secondary" />
|
||||
<Icon name="transform" size="1rem" c="text-secondary" />
|
||||
<Text size="sm">{suggestedTransform.name}</Text>
|
||||
</Flex>
|
||||
<Flex align="center" gap="sm">
|
||||
@@ -251,12 +251,8 @@ function getSourceCode(
|
||||
|
||||
function getTransformUrl(transform: SuggestedTransform): string {
|
||||
return match(transform)
|
||||
.with({ id: P.number }, ({ id }) => Urls.transformQuery(id))
|
||||
.with({ source: { type: "python" } }, () =>
|
||||
Urls.newTransformFromType("python"),
|
||||
)
|
||||
.with({ source: { type: "query" } }, () =>
|
||||
Urls.newTransformFromType("native"),
|
||||
)
|
||||
.with({ id: P.number }, ({ id }) => Urls.transform(id))
|
||||
.with({ source: { type: "python" } }, () => Urls.newPythonTransform())
|
||||
.with({ source: { type: "query" } }, () => Urls.newNativeTransform())
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.todoContainer {
|
||||
border: 1px solid var(--mb-color-brand);
|
||||
background-color: var(--mb-color-brand-lighter);
|
||||
background-color: var(--mb-color-brand-light);
|
||||
}
|
||||
|
||||
.todoHeader {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "metabase/ui";
|
||||
import { useGetSuggestedMetabotPromptsQuery } from "metabase-enterprise/api";
|
||||
import { MetabotResetLongChatButton } from "metabase-enterprise/metabot/components/MetabotChat/MetabotResetLongChatButton";
|
||||
import type { SuggestionModel } from "metabase-enterprise/rich_text_editing/tiptap/extensions/shared/types";
|
||||
|
||||
import { useMetabotAgent, useMetabotChatHandlers } from "../../hooks";
|
||||
import type { MetabotConfig } from "../Metabot";
|
||||
@@ -27,21 +28,21 @@ import { Messages } from "./MetabotChatMessage";
|
||||
import { MetabotThinking } from "./MetabotThinking";
|
||||
import { useScrollManager } from "./hooks";
|
||||
|
||||
const defaultConfig: MetabotConfig = {
|
||||
suggestionModels: [
|
||||
"dataset",
|
||||
"metric",
|
||||
"card",
|
||||
"table",
|
||||
"database",
|
||||
"dashboard",
|
||||
],
|
||||
};
|
||||
const defaultSuggesstionModels: SuggestionModel[] = [
|
||||
"dataset",
|
||||
"metric",
|
||||
"card",
|
||||
"table",
|
||||
"database",
|
||||
"dashboard",
|
||||
];
|
||||
|
||||
export const MetabotChat = ({
|
||||
config = defaultConfig,
|
||||
config,
|
||||
w = "30rem",
|
||||
}: {
|
||||
config?: MetabotConfig;
|
||||
w?: string;
|
||||
}) => {
|
||||
const metabot = useMetabotAgent();
|
||||
const { handleSubmitInput, handleRetryMessage, handleResetInput } =
|
||||
@@ -80,7 +81,7 @@ export const MetabotChat = ({
|
||||
<Sidebar
|
||||
isOpen={metabot.visible}
|
||||
side="right"
|
||||
width="30rem"
|
||||
width={w}
|
||||
aria-hidden={!metabot.visible}
|
||||
>
|
||||
<Box className={Styles.container} data-testid="metabot-chat">
|
||||
@@ -103,7 +104,7 @@ export const MetabotChat = ({
|
||||
<Icon c="text-primary" name="revert" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{!config.preventClose && (
|
||||
{!config?.preventClose && (
|
||||
<ActionIcon
|
||||
onClick={handleClose}
|
||||
data-testid="metabot-close-chat"
|
||||
@@ -133,11 +134,11 @@ export const MetabotChat = ({
|
||||
>
|
||||
<Box component={EmptyDashboardBot} w="6rem" />
|
||||
<Text c="text-light" maw="12rem" ta="center">
|
||||
{config.emptyText ??
|
||||
{config?.emptyText ??
|
||||
t`I can help you explore your metrics and models.`}
|
||||
</Text>
|
||||
</Flex>
|
||||
{!config.hideSuggestedPrompts && (
|
||||
{!config?.hideSuggestedPrompts && (
|
||||
<Stack
|
||||
gap="sm"
|
||||
className={Styles.promptSuggestionsContainer}
|
||||
@@ -202,7 +203,9 @@ export const MetabotChat = ({
|
||||
placeholder={t`Tell me to do something, or ask a question`}
|
||||
onChange={handleEditorChange}
|
||||
onSubmit={handleEditorSubmit}
|
||||
suggestionModels={config.suggestionModels}
|
||||
suggestionModels={
|
||||
config?.suggestionModels ?? defaultSuggesstionModels
|
||||
}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -10,11 +10,11 @@ import { hasPremiumFeature } from "metabase-enterprise/settings";
|
||||
|
||||
import { Metabot } from "./components/Metabot";
|
||||
import { MetabotAdminPage } from "./components/MetabotAdmin/MetabotAdminPage";
|
||||
import { MetabotAdminAppBarButton } from "./components/MetabotAdminAppBarButton";
|
||||
import { MetabotAppBarButton } from "./components/MetabotAppBarButton";
|
||||
import { getMetabotQuickLinks } from "./components/MetabotQuickLinks";
|
||||
import { MetabotToggleButton } from "./components/MetabotToggleButton";
|
||||
import { MetabotContext, MetabotProvider, defaultContext } from "./context";
|
||||
import { useMetabotAgent } from "./hooks";
|
||||
import { getMetabotVisible, metabotReducer } from "./state";
|
||||
|
||||
if (hasPremiumFeature("metabot_v3")) {
|
||||
@@ -50,7 +50,7 @@ if (hasPremiumFeature("metabot_v3")) {
|
||||
|
||||
PLUGIN_METABOT.MetabotToggleButton = MetabotToggleButton;
|
||||
PLUGIN_METABOT.MetabotAppBarButton = MetabotAppBarButton;
|
||||
PLUGIN_METABOT.MetabotAdminAppBarButton = MetabotAdminAppBarButton;
|
||||
PLUGIN_METABOT.useMetabotAgent = useMetabotAgent;
|
||||
|
||||
PLUGIN_REDUCERS.metabotPlugin = metabotReducer;
|
||||
} else if (hasPremiumFeature("offer_metabase_ai")) {
|
||||
|
||||
@@ -130,7 +130,8 @@ export const getProfile = createSelector(
|
||||
return profileOverride;
|
||||
}
|
||||
|
||||
return location.pathname.startsWith(Urls.transformList())
|
||||
const transformsUrl = Urls.transformList();
|
||||
return transformsUrl && location.pathname.startsWith(transformsUrl)
|
||||
? "transforms_codegen"
|
||||
: undefined;
|
||||
},
|
||||
|
||||
@@ -31,12 +31,7 @@ class CollectionRow extends Component {
|
||||
)}
|
||||
{...(collection.archived ? undefined : { onClick: onSelectCollection })}
|
||||
>
|
||||
<Icon
|
||||
name="folder"
|
||||
size={ICON_SIZE}
|
||||
style={{ opacity: 0.25 }}
|
||||
className={CS.flexNoShrink}
|
||||
/>
|
||||
<Icon name="folder" size={ICON_SIZE} className={CS.flexNoShrink} />
|
||||
<Ellipsified className={cx(CS.flexFull, CS.ml1, CS.textBold)} flex={1}>
|
||||
{collection.name}
|
||||
</Ellipsified>
|
||||
|
||||
@@ -74,6 +74,7 @@ if (hasPremiumFeature("snippet_collections")) {
|
||||
const collection = snippetSidebar.props.snippetCollection;
|
||||
return (
|
||||
<CollectionOptionsButton
|
||||
key="options"
|
||||
{...snippetSidebar.props}
|
||||
{...props}
|
||||
setSidebarState={snippetSidebar.setState.bind(snippetSidebar)}
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
.dataPicker {
|
||||
border-right: 1px solid var(--mb-color-border);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.databaseSelector {
|
||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-md);
|
||||
border: 1px solid var(--mb-color-border);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { useEffect, useState } from "react";
|
||||
import { t } from "ttag";
|
||||
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
} from "metabase/api";
|
||||
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
|
||||
import { DatabaseDataSelector } from "metabase/query_builder/components/DataSelector";
|
||||
import { Box, Button, Icon, Stack, Text } from "metabase/ui";
|
||||
import { ActionIcon, Box, Button, Icon, Stack, Text } from "metabase/ui";
|
||||
import type {
|
||||
Database,
|
||||
DatabaseId,
|
||||
@@ -47,6 +48,8 @@ export function PythonDataPicker({
|
||||
getInitialTableSelections(tables),
|
||||
);
|
||||
|
||||
const [isCollapsed, { toggle: toggleCollapse }] = useDisclosure(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTableSelections(getInitialTableSelections(tables));
|
||||
}, [tables]);
|
||||
@@ -141,59 +144,61 @@ export function PythonDataPicker({
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
p="md"
|
||||
gap="md"
|
||||
h="100%"
|
||||
w="30%"
|
||||
miw="300px"
|
||||
className={S.dataPicker}
|
||||
data-testid="python-data-picker"
|
||||
>
|
||||
<Box>
|
||||
<Text fw="bold">{t`Source database`}</Text>
|
||||
<Text size="sm" c="text-light" mb="sm">
|
||||
{t`Select the database that contains your source data.`}
|
||||
</Text>
|
||||
|
||||
<DatabaseDataSelector
|
||||
className={S.databaseSelector}
|
||||
selectedDatabaseId={database}
|
||||
setDatabaseFn={handleDatabaseChange}
|
||||
databases={databases?.data ?? []}
|
||||
databaseIsDisabled={(database: Database) =>
|
||||
!hasFeature(database, "transforms/python")
|
||||
}
|
||||
<Stack p="md" gap="sm">
|
||||
<ActionIcon onClick={toggleCollapse} py="md">
|
||||
<Icon
|
||||
name={isCollapsed ? "chevronright" : "chevronleft"}
|
||||
c={isCollapsed ? "brand" : undefined}
|
||||
/>
|
||||
</Box>
|
||||
{database && (
|
||||
<Box>
|
||||
<Text fw="bold">{t`Pick tables and alias them`}</Text>
|
||||
<Text size="sm" c="text-light" mb="sm">
|
||||
{t`Select tables to use as data sources and provide aliases that can be referenced in your Python script.`}
|
||||
</Text>
|
||||
<Stack gap="md">
|
||||
{tableSelections.map((selection, index) => (
|
||||
<SelectionInput
|
||||
key={index}
|
||||
selection={selection}
|
||||
database={database}
|
||||
tables={tables}
|
||||
usedAliases={usedAliases}
|
||||
availableTables={availableTables}
|
||||
onChange={(selection) =>
|
||||
handleSelectionChange(index, selection)
|
||||
}
|
||||
onRemove={() => handleRemoveTable(index)}
|
||||
disabled={isLoadingTables}
|
||||
/>
|
||||
))}
|
||||
<AddTableButton
|
||||
onClick={handleAddTable}
|
||||
disabled={availableTables.length === 0}
|
||||
</ActionIcon>
|
||||
{!isCollapsed && (
|
||||
<Stack gap="md" w="100%" data-testid="python-data-picker">
|
||||
<Box>
|
||||
<Text fw="bold">{t`Source database`}</Text>
|
||||
<Text size="sm" c="text-light" mb="sm">
|
||||
{t`Select the database that contains your source data.`}
|
||||
</Text>
|
||||
|
||||
<DatabaseDataSelector
|
||||
className={S.databaseSelector}
|
||||
selectedDatabaseId={database}
|
||||
setDatabaseFn={handleDatabaseChange}
|
||||
databases={databases?.data ?? []}
|
||||
databaseIsDisabled={(database: Database) =>
|
||||
!hasFeature(database, "transforms/python")
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
{database && (
|
||||
<Box>
|
||||
<Text fw="bold">{t`Pick tables and alias them`}</Text>
|
||||
<Text size="sm" c="text-light" mb="sm">
|
||||
{t`Select tables to use as data sources and provide aliases that can be referenced in your Python script.`}
|
||||
</Text>
|
||||
<Stack gap="md">
|
||||
{tableSelections.map((selection, index) => (
|
||||
<SelectionInput
|
||||
key={index}
|
||||
selection={selection}
|
||||
database={database}
|
||||
tables={tables}
|
||||
usedAliases={usedAliases}
|
||||
availableTables={availableTables}
|
||||
onChange={(selection) =>
|
||||
handleSelectionChange(index, selection)
|
||||
}
|
||||
onRemove={() => handleRemoveTable(index)}
|
||||
disabled={isLoadingTables}
|
||||
/>
|
||||
))}
|
||||
<AddTableButton
|
||||
onClick={handleAddTable}
|
||||
disabled={availableTables.length === 0}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { Button, Group, Tooltip } from "metabase/ui";
|
||||
import type { PythonTransformSourceDraft } from "metabase-types/api";
|
||||
|
||||
import { getValidationResult } from "./utils";
|
||||
|
||||
type PythonEditorActionsProps = {
|
||||
source: PythonTransformSourceDraft;
|
||||
isSaving: boolean;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export function PythonEditorActions({
|
||||
source,
|
||||
isSaving,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: PythonEditorActionsProps) {
|
||||
const { isValid, errorMessage } = getValidationResult(source);
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Button onClick={onCancel}>{t`Cancel`}</Button>
|
||||
<Tooltip label={errorMessage} disabled={errorMessage == null}>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={!isValid || isSaving}
|
||||
onClick={onSave}
|
||||
>
|
||||
{t`Save`}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./PythonEditorActions";
|
||||
@@ -1,4 +1,4 @@
|
||||
export type QueryValidationResult = {
|
||||
export type ValidationResult = {
|
||||
isValid: boolean;
|
||||
errorMessage?: string;
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import type { PythonTransformSourceDraft } from "metabase-types/api";
|
||||
|
||||
import type { ValidationResult } from "./types";
|
||||
|
||||
export function getValidationResult(
|
||||
source: PythonTransformSourceDraft,
|
||||
): ValidationResult {
|
||||
if (!source["source-database"]) {
|
||||
return { isValid: false, errorMessage: t`Select a source a database` };
|
||||
}
|
||||
|
||||
if (source.body.trim() === "") {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t`The Python script cannot be empty`,
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.keys(source["source-tables"]).length === 0) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t`Select at least one table to alias`,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
@@ -2,10 +2,19 @@ import type { ReactNode } from "react";
|
||||
import { ResizableBox } from "react-resizable";
|
||||
import { t } from "ttag";
|
||||
|
||||
import Link from "metabase/common/components/Link";
|
||||
import { ForwardRefLink } from "metabase/common/components/Link";
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
import RunButtonWithTooltip from "metabase/query_builder/components/RunButtonWithTooltip";
|
||||
import { Box, Button, Checkbox, Flex, Icon, Stack, Tooltip } from "metabase/ui";
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Group,
|
||||
Icon,
|
||||
Stack,
|
||||
Tooltip,
|
||||
} from "metabase/ui";
|
||||
import { SHARED_LIB_IMPORT_PATH } from "metabase-enterprise/transforms-python/constants";
|
||||
|
||||
import { PythonEditor } from "../../PythonEditor";
|
||||
@@ -111,7 +120,11 @@ function MaybeResizableBox({
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
if (!resizable) {
|
||||
return <Box h="100%">{children}</Box>;
|
||||
return (
|
||||
<Box w="100%" h="100%">
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -134,22 +147,20 @@ function SharedLibraryActions({
|
||||
onChange: (source: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<Stack className={S.libraryActions} p="md" gap="sm">
|
||||
<SharedLibraryImportToggle source={source} onChange={onChange} />
|
||||
<Group className={S.libraryActions} p="md" gap="sm">
|
||||
<SharedLibraryImportButton source={source} onChange={onChange} />
|
||||
<SharedLibraryEditLink />
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function SharedLibraryImportToggle({
|
||||
function SharedLibraryImportButton({
|
||||
source,
|
||||
onChange,
|
||||
}: {
|
||||
source: string;
|
||||
onChange: (source: string) => void;
|
||||
}) {
|
||||
const hasSharedLib = hasImport(source, SHARED_LIB_IMPORT_PATH);
|
||||
|
||||
function handleToggleSharedLib() {
|
||||
if (hasImport(source, SHARED_LIB_IMPORT_PATH)) {
|
||||
onChange(removeImport(source, SHARED_LIB_IMPORT_PATH));
|
||||
@@ -159,25 +170,24 @@ function SharedLibraryImportToggle({
|
||||
}
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
label={t`Import common library`}
|
||||
checked={hasSharedLib}
|
||||
onChange={handleToggleSharedLib}
|
||||
size="sm"
|
||||
/>
|
||||
<Tooltip label={t`Import common library`}>
|
||||
<ActionIcon onChange={handleToggleSharedLib}>
|
||||
<Icon name="reference" c="text-dark" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function SharedLibraryEditLink() {
|
||||
return (
|
||||
<Flex
|
||||
component={Link}
|
||||
target="_blank"
|
||||
to={Urls.transformPythonLibrary({ path: SHARED_LIB_IMPORT_PATH })}
|
||||
gap="sm"
|
||||
>
|
||||
<Icon name="pencil" />
|
||||
{t`Edit common library`}
|
||||
</Flex>
|
||||
<Tooltip label={t`Edit common library`}>
|
||||
<ActionIcon
|
||||
component={ForwardRefLink}
|
||||
target="_blank"
|
||||
to={Urls.transformPythonLibrary({ path: SHARED_LIB_IMPORT_PATH })}
|
||||
>
|
||||
<Icon name="pencil" c="text-dark" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.main {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,61 +1,36 @@
|
||||
import { useHotkeys } from "@mantine/hooks";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { PythonTransformEditorProps } from "metabase/plugins";
|
||||
import { Flex, Stack } from "metabase/ui";
|
||||
import { EditorHeader } from "metabase-enterprise/transforms/components/QueryEditor/EditorHeader";
|
||||
import { useRegisterMetabotTransformContext } from "metabase-enterprise/transforms/hooks/use-register-transform-metabot-context";
|
||||
import type {
|
||||
PythonTransformSource,
|
||||
PythonTransformSourceDraft,
|
||||
PythonTransformTableAliases,
|
||||
Table,
|
||||
Transform,
|
||||
} from "metabase-types/api";
|
||||
import { TransformHeader } from "metabase-enterprise/transforms/components/TransformHeader";
|
||||
import { TransformMoreMenuWithModal } from "metabase-enterprise/transforms/components/TransformMoreMenu";
|
||||
import type { PythonTransformTableAliases, Table } from "metabase-types/api";
|
||||
|
||||
import { PythonDataPicker } from "./PythonDataPicker";
|
||||
import { PythonEditorActions } from "./PythonEditorActions";
|
||||
import { PythonEditorBody } from "./PythonEditorBody";
|
||||
import { PythonEditorResults } from "./PythonEditorResults";
|
||||
import S from "./PythonTransformEditor.module.css";
|
||||
import {
|
||||
getValidationResult,
|
||||
isPythonTransformSource,
|
||||
updateTransformSignature,
|
||||
useShouldShowPythonDebugger,
|
||||
useTestPythonTransform,
|
||||
} from "./utils";
|
||||
|
||||
export type PythonTransformEditorProps = {
|
||||
transform?: Transform | undefined;
|
||||
initialSource: PythonTransformSourceDraft;
|
||||
proposedSource?: PythonTransformSource;
|
||||
isNew?: boolean;
|
||||
isSaving?: boolean;
|
||||
isRunnable?: boolean;
|
||||
onChange?: (newSource: PythonTransformSourceDraft) => void;
|
||||
onSave: (newSource: PythonTransformSource) => void;
|
||||
onCancel: () => void;
|
||||
onRejectProposed?: () => void;
|
||||
onAcceptProposed?: (query: PythonTransformSource) => void;
|
||||
};
|
||||
|
||||
export function PythonTransformEditor({
|
||||
transform,
|
||||
initialSource,
|
||||
id,
|
||||
name,
|
||||
source,
|
||||
proposedSource,
|
||||
isNew = true,
|
||||
isSaving = false,
|
||||
isRunnable = true,
|
||||
onChange,
|
||||
isSaving,
|
||||
isSourceDirty,
|
||||
onNameChange,
|
||||
onSourceChange,
|
||||
onSave,
|
||||
onCancel,
|
||||
onRejectProposed,
|
||||
onAcceptProposed,
|
||||
onRejectProposed,
|
||||
}: PythonTransformEditorProps) {
|
||||
const [source, setSource] = useState(initialSource);
|
||||
const saveSource = proposedSource ?? source;
|
||||
const [isSourceDirty, setIsSourceDirty] = useState(false);
|
||||
|
||||
useRegisterMetabotTransformContext(transform, proposedSource ?? source);
|
||||
|
||||
const { isRunning, isDirty, cancel, run, executionResult } =
|
||||
useTestPythonTransform(source);
|
||||
|
||||
@@ -64,9 +39,7 @@ export function PythonTransformEditor({
|
||||
...source,
|
||||
body,
|
||||
};
|
||||
setSource(newSource);
|
||||
setIsSourceDirty(true);
|
||||
onChange?.(newSource);
|
||||
onSourceChange(newSource);
|
||||
};
|
||||
|
||||
const handleDataChange = (
|
||||
@@ -86,25 +59,9 @@ export function PythonTransformEditor({
|
||||
"source-database": database,
|
||||
"source-tables": sourceTables,
|
||||
};
|
||||
setSource(newSource);
|
||||
setIsSourceDirty(true);
|
||||
onSourceChange(newSource);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (isPythonTransformSource(saveSource)) {
|
||||
onSave(saveSource);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAcceptProposed =
|
||||
proposedSource && onAcceptProposed
|
||||
? () => {
|
||||
setSource(proposedSource);
|
||||
setIsSourceDirty(true);
|
||||
onAcceptProposed(proposedSource);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const showDebugger = useShouldShowPythonDebugger();
|
||||
|
||||
const handleCmdEnter = () => {
|
||||
@@ -113,15 +70,13 @@ export function PythonTransformEditor({
|
||||
}
|
||||
if (isRunning) {
|
||||
cancel();
|
||||
} else if (isRunnable && isPythonTransformSource(saveSource)) {
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
};
|
||||
|
||||
useHotkeys([["mod+Enter", handleCmdEnter]], []);
|
||||
|
||||
const validationResult = getValidationResult(saveSource);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
w="100%"
|
||||
@@ -130,43 +85,49 @@ export function PythonTransformEditor({
|
||||
data-testid="transform-query-editor"
|
||||
gap={0}
|
||||
>
|
||||
<EditorHeader
|
||||
isNew={isNew}
|
||||
isSaving={isSaving}
|
||||
hasProposedQuery={!!proposedSource}
|
||||
onSave={handleSave}
|
||||
onCancel={onCancel}
|
||||
validationResult={validationResult}
|
||||
isQueryDirty={isSourceDirty}
|
||||
<TransformHeader
|
||||
id={id}
|
||||
name={name}
|
||||
actions={
|
||||
isSaving || isSourceDirty ? (
|
||||
<PythonEditorActions
|
||||
source={source}
|
||||
isSaving={isSaving}
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
) : id != null ? (
|
||||
<TransformMoreMenuWithModal transformId={id} />
|
||||
) : null
|
||||
}
|
||||
onNameChange={onNameChange}
|
||||
/>
|
||||
<Flex h="100%" w="100%">
|
||||
<Flex className={S.main}>
|
||||
<PythonDataPicker
|
||||
database={saveSource["source-database"]}
|
||||
tables={saveSource["source-tables"]}
|
||||
database={source["source-database"]}
|
||||
tables={source["source-tables"]}
|
||||
onChange={handleDataChange}
|
||||
/>
|
||||
<Stack w="100%" h="100%" gap={0}>
|
||||
<PythonEditorBody
|
||||
isRunnable={isRunnable && isPythonTransformSource(source)}
|
||||
isRunning={isRunning}
|
||||
isDirty={isDirty}
|
||||
onRun={run}
|
||||
onCancel={cancel}
|
||||
source={source.body}
|
||||
proposedSource={proposedSource?.body}
|
||||
onChange={handleScriptChange}
|
||||
withDebugger={showDebugger}
|
||||
onAcceptProposed={handleAcceptProposed}
|
||||
onRejectProposed={onRejectProposed}
|
||||
/>
|
||||
{showDebugger && (
|
||||
<PythonEditorResults
|
||||
isRunning={isRunning}
|
||||
executionResult={executionResult}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<PythonEditorBody
|
||||
isRunnable
|
||||
isRunning={isRunning}
|
||||
isDirty={isDirty}
|
||||
onRun={run}
|
||||
onCancel={cancel}
|
||||
source={source.body}
|
||||
proposedSource={proposedSource?.body}
|
||||
onChange={handleScriptChange}
|
||||
withDebugger={showDebugger}
|
||||
onAcceptProposed={onAcceptProposed}
|
||||
onRejectProposed={onRejectProposed}
|
||||
/>
|
||||
</Flex>
|
||||
{showDebugger && (
|
||||
<PythonEditorResults
|
||||
isRunning={isRunning}
|
||||
executionResult={executionResult}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { getErrorMessage } from "metabase/api/utils";
|
||||
import { useExecutePythonMutation } from "metabase-enterprise/api/transform-python";
|
||||
import type {
|
||||
ExecutePythonTransformResponse,
|
||||
PythonTransformSource,
|
||||
PythonTransformSourceDraft,
|
||||
PythonTransformTableAliases,
|
||||
Table,
|
||||
@@ -213,36 +212,8 @@ export function useTestPythonTransform(
|
||||
};
|
||||
}
|
||||
|
||||
export function isPythonTransformSource(
|
||||
source: PythonTransformSourceDraft,
|
||||
): source is PythonTransformSource {
|
||||
return source.type === "python" && source["source-database"] !== undefined;
|
||||
}
|
||||
|
||||
export function useShouldShowPythonDebugger() {
|
||||
const location = useLocation();
|
||||
const params = new URLSearchParams(location.search);
|
||||
return params.get("debugger") === "1";
|
||||
}
|
||||
|
||||
export function getValidationResult(source: PythonTransformSourceDraft) {
|
||||
if (!source["source-database"]) {
|
||||
return { isValid: false, errorMessage: t`Select a source a database` };
|
||||
}
|
||||
|
||||
if (source.body.trim() === "") {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t`The Python script cannot be empty`,
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.keys(source["source-tables"]).length === 0) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t`Select at least one table to alias`,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: var(--mantine-spacing-sm) var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.alias {
|
||||
font-family: var(--mb-default-monospace-font-family);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { skipToken, useGetTableQuery } from "metabase/api";
|
||||
import Link from "metabase/common/components/Link";
|
||||
import CS from "metabase/css/core/index.css";
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
import { Box, Group, Icon, type IconName, Loader, Stack } from "metabase/ui";
|
||||
import { SplitSection } from "metabase-enterprise/transforms/components/SplitSection";
|
||||
import type { Transform } from "metabase-types/api";
|
||||
|
||||
import S from "./SourceSection.module.css";
|
||||
|
||||
type SourceSectionProps = {
|
||||
transform: Transform;
|
||||
};
|
||||
|
||||
export function SourceSection({ transform }: SourceSectionProps) {
|
||||
const { source } = transform;
|
||||
if (source.type !== "python") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { "source-database": sourceDatabaseId, "source-tables": sourceTables } =
|
||||
source;
|
||||
|
||||
return (
|
||||
<SplitSection
|
||||
label={t`Transform source`}
|
||||
description={t`The data sources for this Python transform, by alias.`}
|
||||
>
|
||||
<Stack p="lg" gap="sm" className={S.grid}>
|
||||
{Object.entries(sourceTables).map(([alias, tableId]) => (
|
||||
<TableWithAlias
|
||||
key={alias}
|
||||
alias={alias}
|
||||
tableId={tableId as number}
|
||||
databaseId={sourceDatabaseId as number}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</SplitSection>
|
||||
);
|
||||
}
|
||||
|
||||
type TableWithAliasProps = {
|
||||
alias: string;
|
||||
tableId: number;
|
||||
databaseId: number;
|
||||
};
|
||||
|
||||
function TableWithAlias({ alias, tableId, databaseId }: TableWithAliasProps) {
|
||||
const { data: table, isLoading } = useGetTableQuery(
|
||||
tableId ? { id: tableId } : skipToken,
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader size="xs" data-testid="loader" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group className={S.alias}>{alias}</Group>
|
||||
<Group gap="sm">
|
||||
{table?.db && (
|
||||
<>
|
||||
<SourceItemLink
|
||||
label={table.db.name}
|
||||
icon="database"
|
||||
to={Urls.dataModelDatabase(table.db.id)}
|
||||
data-testid="source-database-link"
|
||||
/>
|
||||
<SourceItemDivider />
|
||||
</>
|
||||
)}
|
||||
{table?.schema && (
|
||||
<>
|
||||
<SourceItemLink
|
||||
label={table.schema}
|
||||
icon="folder"
|
||||
to={Urls.dataModelSchema(databaseId, table.schema)}
|
||||
/>
|
||||
<SourceItemDivider />
|
||||
</>
|
||||
)}
|
||||
<SourceItemLink
|
||||
label={table?.display_name || table?.name || `Table ${tableId}`}
|
||||
icon="table2"
|
||||
to={table ? Urls.queryBuilderTable(table.id, table.db_id) : undefined}
|
||||
/>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type SourceItemLinkProps = {
|
||||
label: string;
|
||||
icon: IconName;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
function SourceItemLink({ label, icon, to }: SourceItemLinkProps) {
|
||||
return (
|
||||
<Link className={CS.link} to={to ?? ""} disabled={to == null}>
|
||||
<Group gap="xs" align="center" justify="center">
|
||||
<Icon name={icon} />
|
||||
<Box>{label}</Box>
|
||||
</Group>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function SourceItemDivider() {
|
||||
return <Icon name="chevronright" size={8} />;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { setupTableEndpoints } from "__support__/server-mocks";
|
||||
import { renderWithProviders, screen, waitFor } from "__support__/ui";
|
||||
import type { ConcreteTableId, Table, Transform } from "metabase-types/api";
|
||||
import { createMockTable } from "metabase-types/api/mocks";
|
||||
import {
|
||||
createMockPythonTransformSource,
|
||||
createMockTransform,
|
||||
} from "metabase-types/api/mocks/transform";
|
||||
|
||||
import { SourceSection } from "./SourceSection";
|
||||
|
||||
const TABLE_A = createMockTable({
|
||||
id: 1,
|
||||
name: "table_a",
|
||||
display_name: "Table A",
|
||||
schema: "schema_a",
|
||||
});
|
||||
|
||||
const TABLE_B = createMockTable({
|
||||
id: 2,
|
||||
name: "table_b",
|
||||
display_name: "Table B",
|
||||
schema: "schema_b",
|
||||
});
|
||||
|
||||
type SetupOpts = {
|
||||
tables?: Table[];
|
||||
transform?: Transform;
|
||||
};
|
||||
|
||||
function setup({
|
||||
tables = [TABLE_A, TABLE_B],
|
||||
transform = createMockTransform(),
|
||||
}: SetupOpts = {}) {
|
||||
for (const table of tables) {
|
||||
setupTableEndpoints(table);
|
||||
}
|
||||
renderWithProviders(<SourceSection transform={transform} />);
|
||||
}
|
||||
|
||||
describe("SourceSection", () => {
|
||||
it("should render nothing for query transform", () => {
|
||||
setup({
|
||||
transform: createMockTransform({
|
||||
source: {
|
||||
type: "query",
|
||||
query: { type: "query", database: 1, query: { "source-table": 1 } },
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(screen.queryByText("Transform source")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the section with label and description", async () => {
|
||||
setup({
|
||||
transform: createMockTransform({
|
||||
source: createMockPythonTransformSource({
|
||||
sourceTables: {
|
||||
orders: TABLE_A.id as ConcreteTableId,
|
||||
customers: TABLE_B.id as ConcreteTableId,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(screen.getByText("Transform source")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("The data sources for this Python transform, by alias."),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId("loader")).toHaveLength(0);
|
||||
});
|
||||
|
||||
expect(screen.getByText("orders")).toBeInTheDocument();
|
||||
expect(screen.getByText("schema_a")).toBeInTheDocument();
|
||||
expect(screen.getByText("Table A")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("customers")).toBeInTheDocument();
|
||||
expect(screen.getByText("schema_b")).toBeInTheDocument();
|
||||
expect(screen.getByText("Table B")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./SourceSection";
|
||||
@@ -1,53 +1,14 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { AdminNavItem } from "metabase/admin/components/AdminNav";
|
||||
import { ForwardRefLink } from "metabase/common/components/Link";
|
||||
import { Route } from "metabase/hoc/Title";
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
import { PLUGIN_TRANSFORMS_PYTHON } from "metabase/plugins";
|
||||
import { Icon, Menu } from "metabase/ui";
|
||||
import { hasPremiumFeature } from "metabase-enterprise/settings";
|
||||
import { trackTransformCreate } from "metabase-enterprise/transforms/analytics";
|
||||
import { FullWidthTransformPageLayout } from "metabase-enterprise/transforms/pages/TransformPageLayout";
|
||||
|
||||
import { PythonTransformEditor } from "./components/PythonTransformEditor";
|
||||
import { SourceSection } from "./components/SourceSection";
|
||||
import { SHARED_LIB_IMPORT_PATH } from "./constants";
|
||||
import { PythonLibraryEditorPage } from "./pages/PythonLibraryEditorPage";
|
||||
import { PythonRunnerSettingsPage } from "./pages/PythonRunnerSettingsPage";
|
||||
import { getBenchNavItems, getBenchRoutes } from "./routes";
|
||||
|
||||
if (hasPremiumFeature("transforms-python")) {
|
||||
PLUGIN_TRANSFORMS_PYTHON.PythonRunnerSettingsPage = PythonRunnerSettingsPage;
|
||||
PLUGIN_TRANSFORMS_PYTHON.isEnabled = true;
|
||||
PLUGIN_TRANSFORMS_PYTHON.getBenchRoutes = getBenchRoutes;
|
||||
PLUGIN_TRANSFORMS_PYTHON.getBenchNavItems = getBenchNavItems;
|
||||
PLUGIN_TRANSFORMS_PYTHON.TransformEditor = PythonTransformEditor;
|
||||
PLUGIN_TRANSFORMS_PYTHON.SourceSection = SourceSection;
|
||||
|
||||
PLUGIN_TRANSFORMS_PYTHON.getAdminRoutes = () => (
|
||||
<Route component={FullWidthTransformPageLayout}>
|
||||
<Route path="library/:path" component={PythonLibraryEditorPage} />
|
||||
</Route>
|
||||
);
|
||||
|
||||
PLUGIN_TRANSFORMS_PYTHON.getTransformsNavLinks = () => (
|
||||
<AdminNavItem
|
||||
label={t`Python library`}
|
||||
path={Urls.transformPythonLibrary({ path: SHARED_LIB_IMPORT_PATH })}
|
||||
icon="code_block"
|
||||
/>
|
||||
);
|
||||
|
||||
PLUGIN_TRANSFORMS_PYTHON.getCreateTransformsMenuItems = () => (
|
||||
<Menu.Item
|
||||
component={ForwardRefLink}
|
||||
to={Urls.newTransformFromType("python")}
|
||||
leftSection={<Icon name="code_block" />}
|
||||
onClick={() => {
|
||||
trackTransformCreate({
|
||||
triggeredFrom: "transform-page-create-menu",
|
||||
creationType: "python",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t`Python script`}
|
||||
</Menu.Item>
|
||||
);
|
||||
PLUGIN_TRANSFORMS_PYTHON.PythonRunnerSettingsPage = PythonRunnerSettingsPage;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { createBenchAdminRouteGuard } from "metabase/bench/components/utils";
|
||||
import type { BenchNavItem } from "metabase/bench/constants/navigation";
|
||||
import { Route } from "metabase/hoc/Title";
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
|
||||
import { SHARED_LIB_IMPORT_PATH } from "./constants";
|
||||
import { PythonLibraryEditorPage } from "./pages/PythonLibraryEditorPage";
|
||||
|
||||
export function getBenchRoutes() {
|
||||
return (
|
||||
<Route
|
||||
path="library/:path"
|
||||
component={createBenchAdminRouteGuard(
|
||||
"transforms-python",
|
||||
PythonLibraryEditorPage,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function getBenchNavItems(isAdmin: boolean): BenchNavItem[] {
|
||||
if (!isAdmin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: "library",
|
||||
url: Urls.transformPythonLibrary({ path: SHARED_LIB_IMPORT_PATH }),
|
||||
icon: "code_block",
|
||||
getLabel: () => t`Python Library`,
|
||||
getDescription: () =>
|
||||
t`A customizable function library for use with your Python transforms.`,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
.root {
|
||||
.body {
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { Flex, Stack } from "metabase/ui";
|
||||
|
||||
import S from "./ColumnLayout.module.css";
|
||||
|
||||
type ColumnLayoutProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function ColumnLayout({ children }: ColumnLayoutProps) {
|
||||
return (
|
||||
<Flex direction="column" h="100%">
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
type ColumnLayoutBodyProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function ColumnLayoutBody({ children }: ColumnLayoutBodyProps) {
|
||||
return (
|
||||
<Flex
|
||||
className={S.body}
|
||||
flex={1}
|
||||
justify="center"
|
||||
pt="3.5rem"
|
||||
pb="xl"
|
||||
px="xl"
|
||||
bg="bg-light"
|
||||
>
|
||||
<Stack flex={1} gap="3.5rem" maw="50rem">
|
||||
{children}
|
||||
</Stack>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./ColumnLayout";
|
||||
@@ -0,0 +1,68 @@
|
||||
import { push } from "react-router-redux";
|
||||
import { t } from "ttag";
|
||||
|
||||
import { AdminContentTable } from "metabase/common/components/AdminContentTable";
|
||||
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
|
||||
import { useDispatch } from "metabase/lib/redux";
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
import { useListTransformJobTransformsQuery } from "metabase-enterprise/api";
|
||||
import type { Transform, TransformJobId } from "metabase-types/api";
|
||||
|
||||
import { ListEmptyState } from "../../ListEmptyState";
|
||||
import { TitleSection } from "../../TitleSection";
|
||||
|
||||
import S from "./DependenciesSection.module.css";
|
||||
|
||||
type DependenciesSectionProps = {
|
||||
jobId: TransformJobId;
|
||||
};
|
||||
|
||||
export function DependenciesSection({ jobId }: DependenciesSectionProps) {
|
||||
const {
|
||||
data: transforms = [],
|
||||
error,
|
||||
isLoading,
|
||||
} = useListTransformJobTransformsQuery(jobId);
|
||||
|
||||
return (
|
||||
<TitleSection
|
||||
label={t`Transforms`}
|
||||
description={t`Transforms will be run in this order.`}
|
||||
>
|
||||
{isLoading || error != null ? (
|
||||
<LoadingAndErrorWrapper loading={isLoading} error={error} />
|
||||
) : transforms.length === 0 ? (
|
||||
<ListEmptyState label={t`There are no transforms for this job.`} />
|
||||
) : (
|
||||
<TransformTable transforms={transforms} />
|
||||
)}
|
||||
</TitleSection>
|
||||
);
|
||||
}
|
||||
|
||||
type TransformTableProps = {
|
||||
transforms: Transform[];
|
||||
};
|
||||
|
||||
export function TransformTable({ transforms }: TransformTableProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleRowClick = (transform: Transform) => {
|
||||
dispatch(push(Urls.transform(transform.id)));
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminContentTable columnTitles={[t`Transform`, t`Target`]}>
|
||||
{transforms.map((transform) => (
|
||||
<tr
|
||||
key={transform.id}
|
||||
className={S.row}
|
||||
onClick={() => handleRowClick(transform)}
|
||||
>
|
||||
<td className={S.wrap}>{transform.name}</td>
|
||||
<td className={S.wrap}>{transform.target.name}</td>
|
||||
</tr>
|
||||
))}
|
||||
</AdminContentTable>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { BenchPaneHeader } from "metabase/bench/components/BenchPaneHeader";
|
||||
import { BenchNameInput } from "metabase/bench/components/shared/BenchNameInput";
|
||||
import type { ScheduleDisplayType, TransformTagId } from "metabase-types/api";
|
||||
|
||||
import { NAME_MAX_LENGTH } from "../../constants";
|
||||
import { ColumnLayout, ColumnLayoutBody } from "../ColumnLayout";
|
||||
|
||||
import { DependenciesSection } from "./DependenciesSection";
|
||||
import { ScheduleSection } from "./ScheduleSection";
|
||||
import { TagSection } from "./TagSection";
|
||||
import type { TransformJobInfo } from "./types";
|
||||
|
||||
type JobEditorProps = {
|
||||
job: TransformJobInfo;
|
||||
actions?: ReactNode;
|
||||
onNameChange: (name: string) => void;
|
||||
onScheduleChange: (
|
||||
schedule: string,
|
||||
uiDisplayType: ScheduleDisplayType,
|
||||
) => void;
|
||||
onTagListChange: (tagIds: TransformTagId[]) => void;
|
||||
};
|
||||
|
||||
export function JobEditor({
|
||||
job,
|
||||
actions,
|
||||
onNameChange,
|
||||
onScheduleChange,
|
||||
onTagListChange,
|
||||
}: JobEditorProps) {
|
||||
return (
|
||||
<ColumnLayout>
|
||||
<BenchPaneHeader
|
||||
title={
|
||||
<BenchNameInput
|
||||
initialValue={job.name}
|
||||
maxLength={NAME_MAX_LENGTH}
|
||||
onChange={onNameChange}
|
||||
/>
|
||||
}
|
||||
actions={actions}
|
||||
withBorder
|
||||
/>
|
||||
<ColumnLayoutBody>
|
||||
<ScheduleSection job={job} onScheduleChange={onScheduleChange} />
|
||||
<TagSection job={job} onTagsChange={onTagListChange} />
|
||||
{job.id != null && <DependenciesSection jobId={job.id} />}
|
||||
</ColumnLayoutBody>
|
||||
</ColumnLayout>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import { renderWithProviders, screen } from "__support__/ui";
|
||||
import { createMockTransformJob } from "metabase-types/api/mocks";
|
||||
|
||||
import { JobView } from "./JobView";
|
||||
import { JobEditor } from "./JobEditor";
|
||||
import type { TransformJobInfo } from "./types";
|
||||
|
||||
type SetupOpts = {
|
||||
@@ -14,7 +14,6 @@ type SetupOpts = {
|
||||
|
||||
function setup({ job = createMockTransformJob() }: SetupOpts) {
|
||||
const onNameChange = jest.fn();
|
||||
const onDescriptionChange = jest.fn();
|
||||
const onScheduleChange = jest.fn();
|
||||
const onTagListChange = jest.fn();
|
||||
|
||||
@@ -24,10 +23,9 @@ function setup({ job = createMockTransformJob() }: SetupOpts) {
|
||||
}
|
||||
|
||||
renderWithProviders(
|
||||
<JobView
|
||||
<JobEditor
|
||||
job={job}
|
||||
onNameChange={onNameChange}
|
||||
onDescriptionChange={onDescriptionChange}
|
||||
onScheduleChange={onScheduleChange}
|
||||
onTagListChange={onTagListChange}
|
||||
/>,
|
||||
@@ -35,7 +33,6 @@ function setup({ job = createMockTransformJob() }: SetupOpts) {
|
||||
|
||||
return {
|
||||
onNameChange,
|
||||
onDescriptionChange,
|
||||
onScheduleChange,
|
||||
onTagListChange,
|
||||
};
|
||||
@@ -6,16 +6,16 @@ import { getScheduleExplanation } from "metabase/lib/cron";
|
||||
import { useMetadataToasts } from "metabase/metadata/hooks";
|
||||
import { Box, Divider, Group, Tooltip } from "metabase/ui";
|
||||
import { useRunTransformJobMutation } from "metabase-enterprise/api";
|
||||
import { trackTransformJobTriggerManualRun } from "metabase-enterprise/transforms/analytics";
|
||||
import type {
|
||||
ScheduleDisplayType,
|
||||
ScheduleSettings,
|
||||
ScheduleType,
|
||||
} from "metabase-types/api";
|
||||
|
||||
import { RunButton } from "../../../components/RunButton";
|
||||
import { RunStatus } from "../../../components/RunStatus";
|
||||
import { SplitSection } from "../../../components/SplitSection";
|
||||
import { trackTransformJobTriggerManualRun } from "../../../analytics";
|
||||
import { RunButton } from "../../RunButton";
|
||||
import { RunStatus } from "../../RunStatus";
|
||||
import { TitleSection } from "../../TitleSection";
|
||||
import type { TransformJobInfo } from "../types";
|
||||
|
||||
type ScheduleSectionProps = {
|
||||
@@ -31,7 +31,7 @@ export function ScheduleSection({
|
||||
onScheduleChange,
|
||||
}: ScheduleSectionProps) {
|
||||
return (
|
||||
<SplitSection
|
||||
<TitleSection
|
||||
label={t`Schedule`}
|
||||
description={t`Configure when this job should run.`}
|
||||
>
|
||||
@@ -46,7 +46,7 @@ export function ScheduleSection({
|
||||
/>
|
||||
<RunButtonSection job={job} />
|
||||
</Group>
|
||||
</SplitSection>
|
||||
</TitleSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ function RunButtonSection({ job }: RunButtonSectionProps) {
|
||||
return (
|
||||
<Tooltip label={t`This job doesn't have tags to run.`} disabled={hasTags}>
|
||||
<RunButton
|
||||
id={job.id}
|
||||
run={job.last_run}
|
||||
isDisabled={!isSaved || !hasTags}
|
||||
onRun={handleRun}
|
||||
@@ -3,8 +3,8 @@ import { t } from "ttag";
|
||||
import { Box } from "metabase/ui";
|
||||
import type { TransformTagId } from "metabase-types/api";
|
||||
|
||||
import { SplitSection } from "../../../components/SplitSection";
|
||||
import { TagMultiSelect } from "../../../components/TagMultiSelect";
|
||||
import { TagMultiSelect } from "../../TagMultiSelect";
|
||||
import { TitleSection } from "../../TitleSection";
|
||||
import type { TransformJobInfo } from "../types";
|
||||
|
||||
type TagSectionProps = {
|
||||
@@ -14,13 +14,13 @@ type TagSectionProps = {
|
||||
|
||||
export function TagSection({ job, onTagsChange }: TagSectionProps) {
|
||||
return (
|
||||
<SplitSection
|
||||
<TitleSection
|
||||
label={t`Tags to run`}
|
||||
description={t`This job will run all transforms tagged with any of the tags you pick here.`}
|
||||
>
|
||||
<Box p="xl">
|
||||
<TagMultiSelect tagIds={job.tag_ids ?? []} onChange={onTagsChange} />
|
||||
</Box>
|
||||
</SplitSection>
|
||||
</TitleSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./JobEditor";
|
||||
export * from "./types";
|
||||
@@ -8,42 +8,39 @@ import {
|
||||
} from "metabase/forms";
|
||||
import { Box, Button, FocusTrap, Group, Modal, Stack, Text } from "metabase/ui";
|
||||
import { useDeleteTransformJobMutation } from "metabase-enterprise/api";
|
||||
|
||||
import type { TransformJobInfo } from "../../types";
|
||||
import type { TransformJobId } from "metabase-types/api";
|
||||
|
||||
type DeleteJobModalProps = {
|
||||
job: TransformJobInfo;
|
||||
jobId: TransformJobId;
|
||||
onDelete: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function DeleteJobModal({
|
||||
job,
|
||||
jobId,
|
||||
onDelete,
|
||||
onClose,
|
||||
}: DeleteJobModalProps) {
|
||||
return (
|
||||
<Modal title={t`Delete this job?`} opened padding="xl" onClose={onClose}>
|
||||
<FocusTrap.InitialFocus />
|
||||
<DeleteJobForm job={job} onDelete={onDelete} onClose={onClose} />
|
||||
<DeleteJobForm jobId={jobId} onDelete={onDelete} onClose={onClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
type DeleteJobFormProps = {
|
||||
job: TransformJobInfo;
|
||||
jobId: TransformJobId;
|
||||
onDelete: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function DeleteJobForm({ job, onDelete, onClose }: DeleteJobFormProps) {
|
||||
function DeleteJobForm({ jobId, onDelete, onClose }: DeleteJobFormProps) {
|
||||
const [deleteJob] = useDeleteTransformJobMutation();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (job.id != null) {
|
||||
await deleteJob(job.id).unwrap();
|
||||
onDelete();
|
||||
}
|
||||
await deleteJob(jobId).unwrap();
|
||||
onDelete();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -0,0 +1,89 @@
|
||||
import { type MouseEvent, useState } from "react";
|
||||
import { push } from "react-router-redux";
|
||||
import { t } from "ttag";
|
||||
|
||||
import { useDispatch } from "metabase/lib/redux";
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
import { useMetadataToasts } from "metabase/metadata/hooks";
|
||||
import { ActionIcon, Icon, Menu } from "metabase/ui";
|
||||
import type { TransformJobId } from "metabase-types/api";
|
||||
|
||||
import type { JobMoreMenuModalState } from "../../types";
|
||||
|
||||
import { DeleteJobModal } from "./DeleteJobModal";
|
||||
|
||||
type JobMoreMenuProps = {
|
||||
jobId: TransformJobId;
|
||||
onOpenModal: (modal: JobMoreMenuModalState) => void;
|
||||
};
|
||||
|
||||
export function JobMoreMenu({ jobId, onOpenModal }: JobMoreMenuProps) {
|
||||
const handleIconClick = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<ActionIcon onClick={handleIconClick}>
|
||||
<Icon name="ellipsis" />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<Icon name="trash" />}
|
||||
onClick={() => onOpenModal({ jobId, modalType: "delete" })}
|
||||
>
|
||||
{t`Delete`}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
type JobMoreMenuModalProps = {
|
||||
modal: JobMoreMenuModalState;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function JobMoreMenuModal({ modal, onClose }: JobMoreMenuModalProps) {
|
||||
const { sendSuccessToast } = useMetadataToasts();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleDelete = () => {
|
||||
sendSuccessToast(t`Job deleted`);
|
||||
dispatch(push(Urls.transformJobList()));
|
||||
onClose();
|
||||
};
|
||||
|
||||
switch (modal.modalType) {
|
||||
case "delete":
|
||||
return (
|
||||
<DeleteJobModal
|
||||
jobId={modal.jobId}
|
||||
onDelete={handleDelete}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
type JobMoreMenuWithModalProps = {
|
||||
jobId: TransformJobId;
|
||||
};
|
||||
|
||||
export function JobMoreMenuWithModal({ jobId }: JobMoreMenuWithModalProps) {
|
||||
const [modal, setModal] = useState<JobMoreMenuModalState>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<JobMoreMenu jobId={jobId} onOpenModal={setModal} />
|
||||
{modal != null && (
|
||||
<JobMoreMenuModal modal={modal} onClose={() => setModal(undefined)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./JobMoreMenu";
|
||||
@@ -1,3 +0,0 @@
|
||||
.row {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
|
||||
import { useListTransformJobTransformsQuery } from "metabase-enterprise/api";
|
||||
import type { TransformJobId } from "metabase-types/api";
|
||||
|
||||
import { TitleSection } from "../../../components/TitleSection";
|
||||
import { TransformTable } from "../../../components/TransformTable";
|
||||
import { ListEmptyState } from "../../ListEmptyState";
|
||||
|
||||
type DependenciesSectionProps = {
|
||||
jobId: TransformJobId;
|
||||
};
|
||||
|
||||
export function DependenciesSection({ jobId }: DependenciesSectionProps) {
|
||||
const {
|
||||
data: transforms = [],
|
||||
error,
|
||||
isLoading,
|
||||
} = useListTransformJobTransformsQuery(jobId);
|
||||
|
||||
return (
|
||||
<TitleSection
|
||||
label={t`Transforms`}
|
||||
description={t`Transforms will be run in this order.`}
|
||||
>
|
||||
{isLoading || error != null ? (
|
||||
<LoadingAndErrorWrapper loading={isLoading} error={error} />
|
||||
) : transforms.length === 0 ? (
|
||||
<ListEmptyState label={t`There are no transforms for this job.`} />
|
||||
) : (
|
||||
<TransformTable transforms={transforms} />
|
||||
)}
|
||||
</TitleSection>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { t } from "ttag";
|
||||
|
||||
import { BrowserCrumbs } from "metabase/common/components/BrowserCrumbs";
|
||||
import * as Urls from "metabase/lib/urls";
|
||||
|
||||
import type { TransformJobInfo } from "../types";
|
||||
|
||||
type HeaderSectionProps = {
|
||||
job: TransformJobInfo;
|
||||
};
|
||||
|
||||
export function HeaderSection({ job }: HeaderSectionProps) {
|
||||
return (
|
||||
<BrowserCrumbs
|
||||
crumbs={[
|
||||
{
|
||||
title: t`Jobs`,
|
||||
to: Urls.transformJobList(),
|
||||
},
|
||||
{
|
||||
title: job.name,
|
||||
to: job.id != null ? Urls.transformJob(job.id) : "",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Route } from "react-router";
|
||||
|
||||
import { renderWithProviders, screen } from "__support__/ui";
|
||||
import { createMockTransformJob } from "metabase-types/api/mocks";
|
||||
|
||||
import type { TransformJobInfo } from "../types";
|
||||
|
||||
import { HeaderSection } from "./HeaderSection";
|
||||
|
||||
type SetupOpts = {
|
||||
job?: TransformJobInfo;
|
||||
};
|
||||
|
||||
function setup({ job = createMockTransformJob() }: SetupOpts = {}) {
|
||||
renderWithProviders(
|
||||
<Route path="/" component={() => <HeaderSection job={job} />} />,
|
||||
{ withRouter: true },
|
||||
);
|
||||
}
|
||||
|
||||
describe("HeaderSection", () => {
|
||||
it("should render links", () => {
|
||||
setup({ job: createMockTransformJob({ name: "Daily job" }) });
|
||||
expect(screen.getByRole("link", { name: "Jobs" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: "Daily job" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user