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

Tevon/add query params #22

Merged
merged 5 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "query" ADD COLUMN "parameters" JSONB;
1 change: 1 addition & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ model query {
name String
description String?
sql String?
parameters Json?
pipeline Json?
scope ScopeType @default(private)
permissions Json @default("{}")
Expand Down
1 change: 1 addition & 0 deletions backend/src/definitions/user-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const UserQuerySchema = z.object({
description: z.string().optional(),
sql: z.string().optional(),
pipeline: z.any().optional(), // TODO replace with actual pipeline schema
parameters: z.any().optional(), // TODO replace with actual parameter schema
scope: z.enum(["private", "organization"]), // TODO turn this into an enum
permissions: z.record(z.any()), // TODO update
favorited_by: z.array(z.string()),
Expand Down
8 changes: 6 additions & 2 deletions backend/src/user-queries/user-queries.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
UseInterceptors,
HttpException,
HttpStatus,
Query,
} from "@nestjs/common";
import { UserQueriesService } from "./user-queries.service";
import { OrgGuard } from "@/auth/organizations.guard";
Expand Down Expand Up @@ -67,9 +68,12 @@ export class UserQueriesController {
@Get("/:id/run")
@UseGuards(OrgGuard("query"))
@UseInterceptors(TrackingInterceptor)
async run(@Param("id") id: string): Promise<QueryResult<any>> {
async run(
@Param("id") id: string,
@Query() query: any,
): Promise<QueryResult<any>> {
try {
const results = await this.userQueriesService.run(id);
const results = await this.userQueriesService.run(id, query);
return results;
} catch (e) {
throw new HttpException(
Expand Down
30 changes: 27 additions & 3 deletions backend/src/user-queries/user-queries.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export class UserQueriesService {
}

// TODO decide on the correct return type here
async run(id: string): Promise<QueryResult<any>> {
async run(id: string, query): Promise<QueryResult<any>> {
const userQuery = await this.findOne(id);

assert(userQuery, new NotFoundException(`Query not found with id ${id}`));
Expand All @@ -142,13 +142,37 @@ export class UserQueriesService {
userQuery.sql,
`SQL statement not found for query ${userQuery.name}`,
);

const { sql, params } = parseParametersFromSQL(userQuery.sql, query);
const results = await this.postgresAdapterService.run({
databaseId: userQuery.database_id,
sql: userQuery.sql,
sql: sql,
values: params,
});

return results;
}
}
}

const parseParametersFromSQL = (
sql: string,
parameters: Record<string, any>,
): { sql: string; params: any[] } => {
const matches = sql.match(/{{(.*?)}}/g);
let paramIndex = 1;
const params: any[] = [];
console.log("Matches", matches);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rm

if (!matches) {
return { sql, params: [] };
}
matches.forEach((match) => {
const paramName = match.replace("{{", "").replace("}}", "");
if (parameters[paramName] !== undefined) {
sql = sql.replace(match, `$${paramIndex}`);
params.push(parameters[paramName]);
paramIndex++;
}
});

return { sql, params };
};
14 changes: 11 additions & 3 deletions frontend/src/app/queries/[userQueryId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import Loading from "@/app/loading";
import { ErrorDisplay } from "@/components/error-display";
import QueryEditor from "@/components/query/query-editor";
import { QueryHeader } from "@/components/query/query-header";
import QueryParameters, {
Parameter,
} from "@/components/query/query-parameters";
import Table from "@/components/table/table";
import {
useUpdateUserQuery,
Expand All @@ -26,19 +29,23 @@ const Page: React.FC<UserQueryPageProps> = ({ params: { userQueryId } }) => {
isLoading: isLoadingUserQuery,
error: userQueryError,
} = useUserQuery(userQueryId);
const [parameters, setParameters] = useState<Parameter[]>([]);
const [savedParameters, setSavedParameters] = useState<Parameter[]>([]);

useEffect(() => {
setSqlQuery(userQuery?.sql || "");
}, [userQuery, setSqlQuery]);
setParameters(userQuery?.parameters || []);
}, [userQuery, setSqlQuery, setParameters]);

const {
data: results,
isLoading: isLoadingResults,
error: resultsError,
} = useUserQueryResults(userQueryId, userQuery?.sql);
} = useUserQueryResults(userQueryId, userQuery?.sql, savedParameters);

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

const { trigger: updateUserQueryTrigger, isMutating: isUpdatingUserQuery } =
Expand Down Expand Up @@ -71,6 +78,7 @@ const Page: React.FC<UserQueryPageProps> = ({ params: { userQueryId } }) => {
return (
<div className="flex flex-col h-full">
<QueryHeader query={userQuery} />
<QueryParameters parameters={parameters} setParameters={setParameters} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to merge master into this branch to see how this component interacts with the Pipelin/SQL tabs

<QueryEditor value={sqlQuery} onChange={setSqlQuery} />
<div className="flex flex-row items-center">
<Button
Expand Down
170 changes: 170 additions & 0 deletions frontend/src/components/query/query-parameter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import React from "react";
import { Parameter } from "./query-parameters";
import {
Button,
ControlGroup,
FormGroup,
InputGroup,
Popover,
SegmentedControl,
} from "@blueprintjs/core";
import { Label } from "recharts";
import { on } from "events";

interface QueryParameterProps {
parameter: Parameter;
index: number;
removeParameter: (index: number) => void;
saveParameter: (parameter: Partial<Parameter>) => void;
onValueChange: (index: number, value: any) => void;
}

const QueryParameter: React.FC<QueryParameterProps> = (props) => {
const { parameter, saveParameter, onValueChange, index, removeParameter } =
props;

const [popoverOpen, setPopoverOpen] = React.useState(false);
const [fieldValues, setFieldValues] = React.useState({
name: parameter.name,
description: parameter.description,
type: parameter.type,
defaultValue: parameter.defaultValue,
});

const handleFieldChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFieldValues({
...fieldValues,
[e.target.name]: e.target.value,
});
};

const handleConfirm = () => {
saveParameter(fieldValues);
setPopoverOpen(false);
};

const getRightElement = () => {
return (
<Popover
isOpen={popoverOpen}
onInteraction={(nextOpenState) => setPopoverOpen(nextOpenState)}
content={
<div style={{ padding: "10px" }}>
<h5>Edit Parameter</h5>
<SegmentedControl
defaultValue="text"
options={[
{
label: "Text",
value: "text",
},
{
label: "Date",
value: "date",
},
{
label: "Number",
value: "number",
},
]}
onValueChange={(value) => {
setFieldValues({
...fieldValues,
type: value,
});
}}
/>
<FormGroup label="Name" labelFor="name">
<InputGroup
id="name"
name="name"
placeholder="Name"
value={fieldValues.name}
onChange={handleFieldChange}
/>
</FormGroup>
<FormGroup label="Description" labelFor="description">
<InputGroup
id="description"
name="description"
placeholder="Description"
value={fieldValues.description}
onChange={handleFieldChange}
/>
</FormGroup>
<FormGroup label="Default Value" labelFor="defaultValue">
<InputGroup
id="defaultValue"
name="defaultValue"
type={fieldValues.type}
placeholder="Default Value"
value={fieldValues.defaultValue}
onChange={handleFieldChange}
/>
</FormGroup>
<div
style={{
display: "flex",
justifyContent: "flex-end",
marginTop: "10px",
}}
>
<Button
style={{ marginRight: "5px" }}
onClick={() => {
setPopoverOpen(false);
setFieldValues({
name: "",
description: "",
type: "",
defaultValue: "",
});
}}
>
Cancel
</Button>
<Button intent="primary" onClick={handleConfirm}>
Confirm
</Button>
</div>
</div>
}
>
<Button
icon="settings"
minimal={true}
onClick={() => setPopoverOpen(true)}
/>
</Popover>
);
};
return (
<div className="">
<FormGroup
label={
<div className="flex items-center">
<span className="mr-2">{parameter.name}</span>
<Button
icon="cross"
minimal={true}
small={true}
className="ml-auto"
onClick={() => removeParameter(index)}
/>
</div>
}
helperText={parameter.description ? parameter.description : undefined}
>
<InputGroup
small
value={parameter.value || parameter.defaultValue}
type={parameter.type}
onValueChange={(value) => onValueChange(index, value)}
rightElement={getRightElement()}
/>
</FormGroup>
</div>
);
};

export default QueryParameter;
80 changes: 80 additions & 0 deletions frontend/src/components/query/query-parameters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from "react";
import { Button, EditableText, FormGroup } from "@blueprintjs/core";
import { Select } from "@blueprintjs/select";
import * as _ from "lodash";
import QueryParameter from "./query-parameter";

export interface Parameter {
name: string;
description: string;
type: string;
defaultValue: string;
value: any;
}

interface QueryParametersProps {
parameters: Parameter[];
setParameters: (parameters: Parameter[]) => void;
}

const QueryParameters: React.FC<QueryParametersProps> = (props) => {
const { parameters, setParameters } = props;

const saveParameter = (index: number, parameter: Partial<Parameter>) => {
const newParameters = _.cloneDeep(parameters);
newParameters[index] = {
...newParameters[index],
...parameter,
};
setParameters(newParameters);
};

const removeParameter = (index: number) => {
const newParameters = _.cloneDeep(parameters);
newParameters.splice(index, 1);
setParameters(newParameters);
};

const setValue = (index: number, value: any) => {
const newParameters = _.cloneDeep(parameters);
newParameters[index].value = value;
setParameters(newParameters);
};

const addParameter = () => {
setParameters([
...parameters,
{
type: "string",
defaultValue: "",
value: null,
name: "new_parameter",
description: "",
},
]);
};

console.log(parameters);
tevonsb marked this conversation as resolved.
Show resolved Hide resolved
return (
<div className="my-2 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2">
{_.map(parameters, (p, index) => {
return (
<div key={index} className="col-span-1 items-center">
<QueryParameter
parameter={p}
index={index}
saveParameter={(param) => saveParameter(index, param)}
removeParameter={(index) => removeParameter(index)}
onValueChange={setValue}
/>
</div>
);
})}
<div className="col-span-1 flex justify-center items-center">
<Button icon="plus" onClick={addParameter} fill></Button>
</div>
</div>
);
};

export default QueryParameters;
Loading
Loading