diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 3e046e52f5..35ebd62af5 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -22,10 +22,11 @@ import { USER_PATH, RETENTION } from 'App/constants/card'; -import { eventKeys, filtersMap } from 'App/types/filter/newFilter'; +import { eventKeys } from 'App/types/filter/newFilter'; import { renderClickmapThumbnail } from './renderMap'; import Widget from 'App/mstore/types/widget'; import FilterItem from 'Shared/Filters/FilterItem'; +import { Input } from 'antd' interface Props { history: any; @@ -40,7 +41,8 @@ function WidgetForm(props: Props) { params: { siteId, dashboardId } } } = props; - const { metricStore, dashboardStore } = useStore(); + const [aiQuery, setAiQuery] = useState('') + const { metricStore, dashboardStore, aiFiltersStore } = useStore(); const isSaving = metricStore.isSaving; const metric: any = metricStore.instance; const [initialInstance, setInitialInstance] = useState(); @@ -54,7 +56,7 @@ function WidgetForm(props: Props) { const isPathAnalysis = metric.metricType === USER_PATH; const isRetention = metric.metricType === RETENTION; const canAddSeries = metric.series.length < 3; - const eventsLength = metric.series[0].filter.filters.filter((i: any) => i.isEvent).length; + const eventsLength = metric.series[0].filter.filters.filter((i: any) => i && i.isEvent).length; const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); const isPredefined = [ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS].includes( @@ -103,7 +105,7 @@ function WidgetForm(props: Props) { history.replace( withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId) ); - dashboardStore.addWidgetToDashboard( + void dashboardStore.addWidgetToDashboard( dashboardStore.getDashboard(parseInt(dashboardId, 10))!, [savedMetric.metricId] ); @@ -130,6 +132,20 @@ function WidgetForm(props: Props) { metricStore.merge(w.fromJson(initialInstance), false); }; + const fetchResults = () => { + aiFiltersStore.getCardFilters(aiQuery, metric.metricType) + .then((f) => { + metric.createSeries(f.filters); + }) + }; + + const handleKeyDown = (event: any) => { + if (event.key === 'Enter') { + fetchResults(); + } + }; + + const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true'; return (
@@ -237,7 +253,20 @@ function WidgetForm(props: Props) {
)} - + {testingKey ? setAiQuery(e.target.value)} + className="w-full mb-2" + onKeyDown={handleKeyDown} + /> : null} + {aiFiltersStore.isLoading ? ( +
+
+ Loading +
+
+ ) : null} {!isPredefined && (
diff --git a/frontend/app/mstore/aiFiltersStore.ts b/frontend/app/mstore/aiFiltersStore.ts index ce0b75abc4..cfd5e69fe4 100644 --- a/frontend/app/mstore/aiFiltersStore.ts +++ b/frontend/app/mstore/aiFiltersStore.ts @@ -1,11 +1,13 @@ -import { makeAutoObservable } from 'mobx'; -import { aiService } from 'App/services'; import Filter from 'Types/filter'; import { FilterKey } from 'Types/filter/filterType'; -import { filtersMap } from "Types/filter/newFilter"; +import { filtersMap } from 'Types/filter/newFilter'; +import { makeAutoObservable } from 'mobx'; + +import { aiService } from 'App/services'; export default class AiFiltersStore { filters: Record = { filters: [] }; + cardFilters: Record = { filters: [] }; filtersSetKey = 0; isLoading: boolean = false; @@ -18,6 +20,56 @@ export default class AiFiltersStore { this.filtersSetKey += 1; }; + setCardFilters = (filters: Record): void => { + this.cardFilters = filters; + this.filtersSetKey += 1; + }; + + getCardFilters = async (query: string, chartType: string): Promise => { + this.isLoading = true; + try { + const r = await aiService.getCardFilters(query, chartType); + const filterObj = Filter({ + filters: r.filters.map((f: Record) => { + if (f.key === 'fetch') { + return mapFetch(f); + } + if (f.key === 'graphql') { + return mapGraphql(f); + } + + const matchingFilter = Object.keys(filtersMap).find((k) => + f.key === 'metadata' ? `_${f.source}` === k : f.key === k + ); + + if (f.key === 'duration') { + const filter = matchingFilter + ? { ...filtersMap[matchingFilter], ...f } + : { ...f, value: f.value ?? [] }; + return { + ...filter, + value: filter.value + ? filter.value.map((i: string) => parseInt(i, 10) * 60 * 1000) + : null, + }; + } + + return matchingFilter + ? { ...filtersMap[matchingFilter], ...f } + : { ...f, value: f.value ?? [] }; + }), + eventsOrder: r.eventsOrder.toLowerCase(), + }); + + this.setCardFilters(filterObj); + return filterObj.toJS(); + } catch (e) { + console.trace(e); + } finally { + this.isLoading = false; + } + }; + getSearchFilters = async (query: string): Promise => { this.isLoading = true; try { @@ -28,17 +80,28 @@ export default class AiFiltersStore { return mapFetch(f); } if (f.key === 'graphql') { - return mapGraphql(f) + return mapGraphql(f); } - const matchingFilter = Object.keys(filtersMap).find(k => f.key === 'metadata' ? `_${f.source}` === k : f.key === k) + const matchingFilter = Object.keys(filtersMap).find((k) => + f.key === 'metadata' ? `_${f.source}` === k : f.key === k + ); if (f.key === 'duration') { - const filter = matchingFilter ? { ...filtersMap[matchingFilter], ...f } : { ...f, value: f.value ?? [] }; - return { ...filter, value: filter.value ? filter.value.map((i: string) => parseInt(i, 10) * 60 * 1000) : null }; + const filter = matchingFilter + ? { ...filtersMap[matchingFilter], ...f } + : { ...f, value: f.value ?? [] }; + return { + ...filter, + value: filter.value + ? filter.value.map((i: string) => parseInt(i, 10) * 60 * 1000) + : null, + }; } - return matchingFilter ? { ...filtersMap[matchingFilter], ...f } : { ...f, value: f.value ?? [] }; + return matchingFilter + ? { ...filtersMap[matchingFilter], ...f } + : { ...f, value: f.value ?? [] }; }), eventsOrder: r.eventsOrder.toLowerCase(), }); @@ -167,5 +230,5 @@ const mapGraphql = (filter: Record) => { return { ...defaultGraphqlFilter, filters: updateFilters(defaultGraphqlFilter.filters, filter.filters), - } -} + }; +}; diff --git a/frontend/app/mstore/types/filter.ts b/frontend/app/mstore/types/filter.ts index 6f6e5dba97..8b77e4f5f5 100644 --- a/frontend/app/mstore/types/filter.ts +++ b/frontend/app/mstore/types/filter.ts @@ -76,6 +76,15 @@ export default class Filter { return this } + fromData(data) { + this.name = data.name + this.filters = data.filters.map((i: Record) => + new FilterItem(undefined, this.isConditional, this.isMobile).fromData(i) + ) + this.eventsOrder = data.eventsOrder + return this + } + toJsonDrilldown() { const json = { name: this.name, diff --git a/frontend/app/mstore/types/filterItem.ts b/frontend/app/mstore/types/filterItem.ts index 354790f45a..19426ecea0 100644 --- a/frontend/app/mstore/types/filterItem.ts +++ b/frontend/app/mstore/types/filterItem.ts @@ -61,6 +61,27 @@ export default class FilterItem { }); } + fromData(data: any) { + this.type = data.type + this.key = data.key + this.label = data.label + this.operatorOptions = data.operatorOptions + this.hasSource = data.hasSource + this.category = data.category + this.sourceOperatorOptions = data.sourceOperatorOptions + this.value = data.value + this.isEvent = Boolean(data.isEvent) + this.operator = data.operator + this.source = data.source + this.sourceOperator = data.sourceOperator + this.filters = data.filters + this.isActive = Boolean(data.isActive) + this.completed = data.completed + this.dropped = data.dropped + + return this + } + fromJson(json: any, mainFilterKey = '') { const isMetadata = json.type === FilterKey.METADATA; let _filter: any = (isMetadata ? filtersMap['_' + json.source] : filtersMap[json.type]) || {}; diff --git a/frontend/app/mstore/types/filterSeries.ts b/frontend/app/mstore/types/filterSeries.ts index 46d6f64726..6a42d10b9c 100644 --- a/frontend/app/mstore/types/filterSeries.ts +++ b/frontend/app/mstore/types/filterSeries.ts @@ -28,6 +28,13 @@ export default class FilterSeries { return this } + fromData(data) { + this.seriesId = data.seriesId + this.name = data.name + this.filter = new Filter().fromData(data.filter) + return this + } + toJson() { return { seriesId: this.seriesId, diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index 3cd2c22416..511794cfa0 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -123,12 +123,21 @@ export default class Widget { this.series.splice(index, 1); } + setSeries(series: FilterSeries[]) { + this.series = series; + } + addSeries() { const series = new FilterSeries(); series.name = 'Series ' + (this.series.length + 1); this.series.push(series); } + createSeries(filters: Record) { + const series = new FilterSeries().fromData({ filter: { filters } , name: 'AI Query', seriesId: 1 }) + this.setSeries([series]) + } + fromJson(json: any, period?: any) { json.config = json.config || {}; runInAction(() => { diff --git a/frontend/app/services/AiService.ts b/frontend/app/services/AiService.ts index ad1cef8591..85feef6772 100644 --- a/frontend/app/services/AiService.ts +++ b/frontend/app/services/AiService.ts @@ -31,4 +31,13 @@ export default class AiService extends BaseService { const { data } = await r.json(); return data; } + + async getCardFilters(query: string, chartType: string): Promise> { + const r = await this.client.post('/intelligent/search-plus', { + question: query, + chartType + }); + const { data } = await r.json(); + return data; + } }