Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grant/bru-1042 #21

Merged
merged 11 commits into from
Mar 30, 2024
38 changes: 23 additions & 15 deletions backend/src/definitions/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,27 @@ import { z } from "zod";
import { ExternalColumnSchema } from ".";

// SCHEMA INFERENCE:
export const InferredSchemaColumnSchema = ExternalColumnSchema.extend({
table: z.string(),
});
export type InferredSchemaColumn = z.infer<typeof InferredSchemaColumnSchema>;

// A relation used in a pipeline. Includes the API path to the relation from the base object defintiion and the object definition ID of the related object defintion
// A relation used in a pipeline. Includes the id of the table being related in, the relation id, and the alias of the related table
export const InferredSchemaRelationSchema = z.object({
api_path: z.string(),
object_definition_id: z.string(),
relation_name: z.string(),
table: z.string(),
relation: z.string(),
as: z.string(),
});
export type InferredSchemaRelation = z.infer<
typeof InferredSchemaRelationSchema
>;

// Given a base external_column object, this schema includes the table and optional relation that the column is from
export const InferredSchemaColumnSchema = ExternalColumnSchema.extend({
table: z.string(),
relation: InferredSchemaRelationSchema.optional(),
});
export type InferredSchemaColumn = z.infer<typeof InferredSchemaColumnSchema>;

// Includes a list of inferred pipeline output properties and the relations used in the pipeline
export const InferredSchemaSchema = z.object({
relations: z.array(InferredSchemaRelationSchema),
properties: z.array(InferredSchemaColumnSchema),
columns: z.array(InferredSchemaColumnSchema),
});
export type InferredSchema = z.infer<typeof InferredSchemaSchema>;

Expand Down Expand Up @@ -116,16 +118,22 @@ export const AggregateStepSchema = z.object({
type: z.literal(StepIdentifierEnum.Aggregate),
group: z.array(InferredSchemaColumnSchema),
operation: AggregationOperationsEnumSchema,
property: z.string(),
column: InferredSchemaColumnSchema,
as: z.string(),
});
export type AggregateStep = z.infer<typeof AggregateStepSchema>;

// Generic condition schema with property, operator, and value
export const ConditionSchema = z.object({
property: z.string(),
column: InferredSchemaColumnSchema,
operator: OperatorsEnumSchema,
value: z.union([z.string(), z.number(), z.boolean(), z.undefined()]),
value: z.union([
z.string(),
z.number(),
z.boolean(),
z.undefined(),
InferredSchemaColumnSchema,
]),
});
export type Condition = z.infer<typeof ConditionSchema>;

Expand Down Expand Up @@ -236,7 +244,7 @@ export type DeriveStep = z.infer<typeof DeriveStepSchema>;

// Order Step
export const OrderPropertySchema = z.object({
property: z.string(),
column: InferredSchemaColumnSchema,
direction: z.enum(["asc", "desc"]),
});
export type OrderProperty = z.infer<typeof OrderPropertySchema>;
Expand All @@ -258,7 +266,7 @@ export type TakeStep = z.infer<typeof TakeStepSchema>;
// Relate Step
export const RelateStepSchema = z.object({
type: z.literal(StepIdentifierEnum.Relate),
relation: z.string(),
relation: InferredSchemaRelationSchema,
});
export type RelateStep = z.infer<typeof RelateStepSchema>;

Expand Down
91 changes: 83 additions & 8 deletions frontend/src/app/queries/[userQueryId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
"use client";
import { Pipeline } from "@/definitions/pipeline";
import { Button, Callout, Drawer, Tab, Tabs } from "@blueprintjs/core";
import Loading from "@/app/loading";
import { ErrorDisplay } from "@/components/error-display";
import QueryBuilder from "@/components/query/query-builder/query-builder";
import QueryEditor from "@/components/query/query-editor";
import { QueryHeader } from "@/components/query/query-header";
import Table from "@/components/table/table";
import {
usePipelineSchema,
useUpdateUserQuery,
useUserQuery,
useUserQueryResults,
} from "@/data/use-user-query";
import { Button, Callout } from "@blueprintjs/core";
import * as _ from "lodash";
import React, { useEffect, useState } from "react";
import * as _ from "lodash";

interface UserQueryPageProps {
params: {
userQueryId: string;
};
}

enum QueryTabEnum {
SQL = "sql",
PIPELINE = "pipeline",
}

const Page: React.FC<UserQueryPageProps> = ({ params: { userQueryId } }) => {
const [sqlQuery, setSqlQuery] = useState("");
const [tab, setTab] = useState<QueryTabEnum>(QueryTabEnum.SQL);
const [drawerToggle, setDrawerToggle] = useState<boolean>(false);
const [sqlQuery, setSqlQuery] = useState<string>("");
const [pipeline, setPipeline] = useState<Pipeline>({ from: "", steps: [] });
const {
data: userQuery,
isLoading: isLoadingUserQuery,
Expand All @@ -37,22 +48,28 @@ const Page: React.FC<UserQueryPageProps> = ({ params: { userQueryId } }) => {
error: resultsError,
} = useUserQueryResults(userQueryId, userQuery?.sql);

const {
data: pipelineSchema,
isLoading: isLoadingPipelineSchema,
error: pipelineSchemaError,
} = usePipelineSchema(pipeline);

const handleSaveQuery = () => {
updateUserQueryTrigger({ sql: sqlQuery });
};

const { trigger: updateUserQueryTrigger, isMutating: isUpdatingUserQuery } =
useUpdateUserQuery(userQueryId);

if (isLoadingUserQuery) {
if (isLoadingUserQuery || isLoadingPipelineSchema) {
return <Loading />;
}

if (userQueryError) {
if (userQueryError || pipelineSchemaError) {
return (
<ErrorDisplay
title="An unexpected error occurred"
description={userQueryError.message}
description={userQueryError || pipelineSchemaError}
/>
);
}
Expand All @@ -71,12 +88,63 @@ const Page: React.FC<UserQueryPageProps> = ({ params: { userQueryId } }) => {
return (
<div className="flex flex-col h-full">
<QueryHeader query={userQuery} />
<QueryEditor value={sqlQuery} onChange={setSqlQuery} />
<div className="flex flex-row items-center justify-between mb-2">
<Tabs
animate
selectedTabId={tab}
id="section-tabs"
key="horizontal"
renderActiveTabPanelOnly={true}
onChange={(tabId: QueryTabEnum) => {
if (tabId === QueryTabEnum.SQL) {
setDrawerToggle(false);
}
setTab(tabId);
}}
>
<Tab
id={QueryTabEnum.SQL}
title={<Button className="bp5-minimal" icon="code" text="SQL" />}
/>
<Tab
id={QueryTabEnum.PIPELINE}
title={
<Button
className="bp5-minimal"
icon="form"
text="Query Builder"
/>
}
/>
</Tabs>
{tab === QueryTabEnum.PIPELINE && (
<Button
className="mt-1"
icon="drawer-left"
onClick={() => setDrawerToggle(true)}
>
Open in drawer
</Button>
)}
</div>
{tab === QueryTabEnum.SQL ? (
<QueryEditor value={sqlQuery} onChange={setSqlQuery} />
) : (
<QueryBuilder
className="overflow-y-auto h-[400px] flex flex-col p-3 gap-y-2"
pipeline={pipeline}
setPipeline={setPipeline}
/>
)}
<div className="flex flex-row items-center">
<Button
className="my-2 mr-2 w-fit "
loading={isUpdatingUserQuery}
disabled={isLoadingResults || isUpdatingUserQuery}
disabled={
isLoadingResults ||
isUpdatingUserQuery ||
(tab === QueryTabEnum.PIPELINE && !pipelineSchema!.success)
}
onClick={handleSaveQuery}
>
Save and run
Expand All @@ -95,6 +163,13 @@ const Page: React.FC<UserQueryPageProps> = ({ params: { userQueryId } }) => {
isLoadingResults={isLoadingResults}
resultsError={undefined}
/>
<Drawer isOpen={drawerToggle} onClose={() => setDrawerToggle(false)}>
<QueryBuilder
className="flex flex-col h-full p-3 overflow-y-auto gap-y-2"
pipeline={pipeline}
setPipeline={setPipeline}
/>
</Drawer>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";
import { InferredSchemaColumn } from "@/definitions/pipeline";
import { IconName, MenuItem, Text } from "@blueprintjs/core";
import SquareIcon, { SquareIconSize } from "@/components/icon/square-icon";
import { ErrorDisplay } from "@/components/error-display";
import Loading from "@/app/loading";
import { HydratedTable } from "@/definitions";

export default function MultiColumnSelectorListItem({
column,
table,
handleClick,
selected,
}: {
column: InferredSchemaColumn;
table?: HydratedTable;
handleClick: any;
selected: InferredSchemaColumn[];
}) {
function renderContent() {
return (
<div className="flex flex-row items-center w-fit">
{table ? (
<SquareIcon
icon={table.icon as IconName}
color={table.color}
size={SquareIconSize.SMALL}
/>
) : (
<SquareIcon
icon="function"
color="gray"
size={SquareIconSize.SMALL}
/>
)}
<div className="flex flex-row items-center py-1 ml-2 w-fit">
{table ? <Text className="mr-1">{table.name}</Text> : null}
<Text className="font-bold">{column.name}</Text>
</div>
</div>
);
}

return (
<MenuItem
key={`${column.table}.${column.name}`}
roleStructure="listoption"
selected={
!!selected.find(
(selectedCol: InferredSchemaColumn) =>
selectedCol.name === column.name &&
selectedCol.table === column.table,
)
}
shouldDismissPopover={false}
text={renderContent()}
onClick={handleClick}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";
import { InferredSchemaColumn } from "@/definitions/pipeline";
import { Tag, Popover, Text, IconName } from "@blueprintjs/core";
import SquareIcon, { SquareIconSize } from "@/components/icon/square-icon";
import { HydratedTable } from "@/definitions";

export default function MultiColumnSelectorTag({
column,
table,
}: {
column: InferredSchemaColumn;
table?: HydratedTable;
}) {
return (
<Tag key={`${column.table}.${column.name}`} className="cursor-default">
<div className="flex flex-row items-center w-fit">
{table ? (
<SquareIcon
icon={table.icon as IconName}
color={table.color}
size={SquareIconSize.SMALL}
/>
) : (
<SquareIcon
icon="function"
color="gray"
size={SquareIconSize.SMALL}
/>
)}
<div className="flex flex-row items-center py-1 ml-1 w-fit">
{table ? <Text className="mr-1">{table.name}</Text> : null}
<Text className="font-bold">{column.name}</Text>
</div>
</div>
</Tag>
);
}
Loading
Loading