Skip to content

Commit 84f3285

Browse files
author
FalkWolsky
committed
Adding Display to show where queries are used
1 parent 9311e81 commit 84f3285

File tree

2 files changed

+230
-10
lines changed

2 files changed

+230
-10
lines changed

client/packages/lowcoder/src/comps/queries/queryComp/queryPropertyView.tsx

Lines changed: 228 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,39 @@ import { ResourceDropdown } from "../resourceDropdown";
3131
import { NOT_SUPPORT_GUI_SQL_QUERY, SQLQuery } from "../sqlQuery/SQLQuery";
3232
import { StreamQuery } from "../httpQuery/streamQuery";
3333
import SupaDemoDisplay from "../../utils/supademoDisplay";
34+
import _ from "lodash";
35+
import React from "react";
36+
import styled from "styled-components";
37+
import { DataSourceButton } from "pages/datasource/pluginPanel";
38+
import { Tooltip, Divider } from "antd";
39+
import { uiCompRegistry } from "comps/uiCompRegistry";
40+
41+
const Wrapper = styled.div`
42+
width: 100%;
43+
padding: 16px;
44+
background-color: white;
45+
46+
.section-title {
47+
font-size: 13px;
48+
line-height: 1.5;
49+
color: grey;
50+
margin-bottom: 8px;
51+
}
52+
53+
.section {
54+
margin-bottom: 12px;
55+
56+
&:last-child {
57+
margin-bottom: 0px;
58+
}
59+
}
60+
`;
61+
62+
const ComponentListWrapper = styled.div`
63+
display: flex;
64+
flex-wrap: wrap;
65+
gap: 8px;
66+
`;
3467

3568
export function QueryPropertyView(props: { comp: InstanceType<typeof QueryComp> }) {
3669
const { comp } = props;
@@ -128,6 +161,12 @@ export function QueryPropertyView(props: { comp: InstanceType<typeof QueryComp>
128161
})}
129162
</>
130163
</QuerySectionWrapper>
164+
165+
<QuerySectionWrapper>
166+
<QueryUsagePropertyView comp={comp} />
167+
</QuerySectionWrapper>
168+
169+
131170
</QueryPropertyViewWrapper>
132171
),
133172
},
@@ -187,16 +226,6 @@ export const QueryGeneralPropertyView = (props: {
187226
comp.children.datasourceId.dispatchChangeValueAction(QUICK_REST_API_ID);
188227
}
189228

190-
// transfer old Lowcoder API datasource to new
191-
const oldLowcoderId = useMemo(
192-
() =>
193-
datasource.find(
194-
(d) =>
195-
d.datasource.creationSource === 2 && OLD_LOWCODER_DATASOURCE.includes(d.datasource.type)
196-
)?.datasource.id,
197-
[datasource]
198-
);
199-
200229
return (
201230
<QueryPropertyViewWrapper>
202231
<QuerySectionWrapper>
@@ -423,6 +452,195 @@ export const QueryGeneralPropertyView = (props: {
423452
);
424453
};
425454

455+
function findQueryInNestedStructure(
456+
structure: any,
457+
queryName: string,
458+
visited = new Set()
459+
) : boolean {
460+
if (typeof structure === "object" && structure !== null) {
461+
if (visited.has(structure)) {
462+
return false;
463+
}
464+
visited.add(structure);
465+
}
466+
467+
if (typeof structure === "string") {
468+
// Regex to match query name in handlebar-like expressions
469+
const regex = new RegExp(
470+
`{{\\s*[!?]?(\\s*${queryName}\\b(\\.[^}\\s]*)?\\s*)(\\?[^}:]*:[^}]*)?\\s*}}`
471+
);
472+
return regex.test(structure);
473+
}
474+
475+
if (typeof structure === "object" && structure !== null) {
476+
// Recursively check all properties of the object
477+
return Object.values(structure).some((value) =>
478+
findQueryInNestedStructure(value, queryName, visited)
479+
);
480+
}
481+
return false;
482+
}
483+
484+
function collectComponentsUsingQuery(comps: any, queryName: string) {
485+
486+
// Select all active components
487+
const components = Object.values(comps);
488+
489+
// Filter components that reference the query by name
490+
const componentsUsingQuery = components.filter((component: any) => {
491+
return findQueryInNestedStructure(component.children, queryName);
492+
});
493+
494+
return componentsUsingQuery;
495+
}
496+
497+
// this function we use to gather informations of the places where a Data Query is used.
498+
function collectQueryUsageDetails(component: any, queryName: string): any[] {
499+
const results: any[] = [];
500+
const visited = new WeakSet(); // Track visited objects to avoid circular references
501+
502+
function traverse(node: any, path: string[] = []): boolean {
503+
504+
if (!node || typeof node !== "object") { return false; }
505+
// Avoid circular references
506+
if ( visited.has(node)) { return false; }
507+
else { visited.add(node); }
508+
509+
// Check all properties of the current node
510+
for (const [key, value] of Object.entries(node)) {
511+
const currentPath = [...path, key];
512+
if (typeof value === "string" && !key.includes("__") && key != "exposingValues" && key != "ref" && key != "version" && key != "prevContextVal") {
513+
// Check if the string contains the query
514+
const regex = new RegExp(`{{\\s*[!?]?(\\s*${queryName}\\b(\\.[^}\\s]*)?\\s*)(\\?[^}:]*:[^}]*)?\\s*}}`);
515+
const entriesToRemove = ["children", "comp", "unevaledValue", "value"];
516+
if (regex.test(value)) {
517+
console.log("tester",component.children);
518+
results.push({
519+
componentType: component.children.compType?.value || "Unknown Component",
520+
componentName: component.children.name?.value || "Unknown Component",
521+
path: currentPath.filter(entry => !entriesToRemove.includes(entry)).join(" > "),
522+
value,
523+
});
524+
return true; // Stop traversal of this branch
525+
}
526+
} else if (typeof value === "object" && !key.includes("__") && key != "exposingValues" && key != "ref" && key != "version" && key != "prevContextVal") {
527+
// Traverse deeper only through selected properties.
528+
traverse(value, currentPath);
529+
}
530+
}
531+
return false; // Continue traversal if no match is found
532+
}
533+
534+
traverse(component);
535+
return results;
536+
}
537+
538+
function buildQueryUsageDataset(components: any[], queryName: string): any[] {
539+
const dataset: any[] = [];
540+
const visitedComponents = new WeakSet(); // Prevent revisiting components
541+
for (const component of components) {
542+
if (visitedComponents.has(component.children.name)) {
543+
continue;
544+
}
545+
visitedComponents.add(component.children.name);
546+
const usageDetails = collectQueryUsageDetails(component, queryName);
547+
dataset.push(...usageDetails);
548+
}
549+
550+
return dataset;
551+
}
552+
553+
const ComponentButton = (props: {
554+
componentType: string;
555+
componentName: string;
556+
path: string;
557+
value: string;
558+
onSelect: (componentType: string, componentName: string, path: string) => void;
559+
}) => {
560+
const handleClick = () => {
561+
props.onSelect(props.componentType, props.componentName, props.path);
562+
};
563+
564+
// Retrieve the component's icon from the registry
565+
const Icon = uiCompRegistry[props.componentType]?.icon;
566+
567+
return (
568+
<Tooltip title={props.path} placement="top">
569+
<DataSourceButton onClick={handleClick}>
570+
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
571+
{Icon && <Icon style={{ width: "32px"}} />}
572+
<div>
573+
<div style={{ fontSize: "14px", fontWeight: "bold" }}>{props.componentName}</div>
574+
<div style={{ fontSize: "12px", fontWeight: "400" }}>{props.componentType}</div>
575+
</div>
576+
</div>
577+
</DataSourceButton>
578+
</Tooltip>
579+
);
580+
};
581+
582+
export function ComponentUsagePanel(props: {
583+
components: { componentType: string, componentName: string; path: string; value: string }[];
584+
onSelect: (componentType: string, componentName: string, path: string) => void;
585+
}) {
586+
const { components, onSelect } = props;
587+
588+
return (
589+
<Wrapper>
590+
<div className="section-title">{trans("query.componentsUsingQuery")}</div>
591+
<div className="section">
592+
<ComponentListWrapper>
593+
{components.map((component, idx) => (
594+
<ComponentButton
595+
componentType={component.componentType}
596+
componentName={component.componentName}
597+
path={component.path}
598+
value={component.value}
599+
onSelect={onSelect}
600+
/>
601+
))}
602+
</ComponentListWrapper>
603+
</div>
604+
</Wrapper>
605+
);
606+
}
607+
608+
// a usage display to show which components make use of this query
609+
export const QueryUsagePropertyView = (props: {
610+
comp: InstanceType<typeof QueryComp>;
611+
placement?: PageType;
612+
}) => {
613+
const { comp, placement = "editor" } = props;
614+
const editorState = useContext(EditorContext);
615+
const queryName = comp.children.name.getView();
616+
const componentsUsingQuery = collectComponentsUsingQuery(editorState.getAllUICompMap(), queryName);
617+
618+
const usageObjects = buildQueryUsageDataset(componentsUsingQuery, queryName);
619+
620+
const handleSelect = (componentType: string,componentName: string, path: string) => {
621+
editorState.setSelectedCompNames(new Set([componentName]));
622+
// console.log(`Selected Component: ${componentName}, Path: ${path}`);
623+
};
624+
625+
if (usageObjects.length > 0) {
626+
return (
627+
<>
628+
<Divider />
629+
<QuerySectionWrapper>
630+
<QueryConfigWrapper>
631+
<QueryConfigLabel>{trans("query.componentsUsingQueryTitle")}</QueryConfigLabel>
632+
<ComponentUsagePanel components={usageObjects} onSelect={handleSelect} />
633+
</QueryConfigWrapper>
634+
</QuerySectionWrapper>
635+
</>
636+
);
637+
} else {
638+
return <div></div>;
639+
}
640+
641+
};
642+
643+
426644
function useDatasourceStatus(datasourceId: string, datasourceType: ResourceType) {
427645
const datasource = useSelector(getDataSource);
428646
const datasourceTypes = useSelector(getDataSourceTypes);

client/packages/lowcoder/src/i18n/locales/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,8 @@ export const en = {
844844
"categoryWebscrapers" : "Webscrapers & Open Data",
845845
"categoryDocumentHandling" : "Report & Document Generation",
846846
"categoryRPA" : "Robotic Process Automation",
847+
"componentsUsingQueryTitle" : "Query Usage",
848+
"componentsUsingQuery" : "Where is this Query in use"
847849
},
848850

849851

0 commit comments

Comments
 (0)