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

Query endpoint: Raw data modal #1025

Merged
merged 25 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
43bdf5d
endpoint validation and helpers
adrianmroz-allegro Jan 31, 2023
2f33cc0
timezone is optional and we need to support old parameter dataCubeName
adrianmroz-allegro Jan 31, 2023
e657df3
pass whole settings and avoid functional-object pattern mismatch
adrianmroz-allegro Feb 1, 2023
6052d96
tests for query endpoint
adrianmroz-allegro Feb 1, 2023
98cc42f
organize utils
adrianmroz-allegro Feb 1, 2023
7417818
split running query and obtaining query decorator (with some partial …
adrianmroz-allegro Feb 1, 2023
19ff58d
tests for mkurl for invalid inputs
adrianmroz-allegro Feb 1, 2023
5cd7fdd
viewDefinition2 fixtures for mkurl tests
adrianmroz-allegro Feb 1, 2023
f0c95d4
timekeeper fixture for wiki data cube
adrianmroz-allegro Feb 1, 2023
f2e6f6c
normalize view definition mocks across wiki data cube and syntetic cube
adrianmroz-allegro Feb 1, 2023
d7b51c8
use fixtures in query tests
adrianmroz-allegro Feb 1, 2023
273530e
better wording
adrianmroz-allegro Feb 1, 2023
ebf1c51
tests for handle-request-errors
adrianmroz-allegro Feb 2, 2023
c61c753
endpoints test only endpoint logic, controllers are tested e2e
adrianmroz-allegro Feb 2, 2023
b6fc886
ApiContext
adrianmroz-allegro Feb 1, 2023
c404e50
expose ApiContext inside turnilo application
adrianmroz-allegro Feb 1, 2023
0854ead
use new ApiContext in data visualization
adrianmroz-allegro Feb 1, 2023
ebb6cad
nest query endpoints and prepare one for boolean
adrianmroz-allegro Feb 3, 2023
317aaea
boolean filter query endpoint and usage
adrianmroz-allegro Feb 3, 2023
31dd984
fix tests
adrianmroz-allegro Feb 3, 2023
31759bc
extract api call creation
adrianmroz-allegro Feb 3, 2023
4c6bc85
reuse type
adrianmroz-allegro Feb 3, 2023
3e539a3
fix spread of arguments
adrianmroz-allegro Feb 3, 2023
5511f71
query method for raw data modal
adrianmroz-allegro Feb 3, 2023
897e6da
Merge branch 'master' into feature/raw-data-modal-query-endpoint
adrianmroz-allegro Feb 8, 2023
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
52 changes: 52 additions & 0 deletions cypress/e2e/mkurl.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2017-2022 Allegro.pl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
ViewDefinitionConverter2Fixtures
} from "../../src/common/view-definitions/version-2/view-definition-converter-2.fixtures";
import { total } from "../../src/common/view-definitions/version-4/view-definition-4.fixture";

interface MkurlResponse {
hash: string;
}

context("mkurl", () => {
it("should return hash for version 4 view definition", () => {
const body = {
dataCubeName: "wiki",
viewDefinitionVersion: "4",
viewDefinition: total
};

cy.request("POST", "http://localhost:9090/mkurl", body).then((hash: Cypress.Response<MkurlResponse>) => {
expect(hash.status).to.eq(200);
expect(hash.body.hash).to.eq("#wiki/4/N4IgbglgzgrghgGwgLzgFwgewHYgFwhqZqJQgA0408SqGOAygKZobYDmZe2MCClGALZNkOJvhABRNAGMA9AFUAKgGEKIAGYQEaJgCcuAbVBoAngAdxBIeMp6mGiTfU2ACvqwATI6E8w96Fi4BK4AjAAi6lC65vgAtKECFlaaCJiY9p4gAL4AunmUUOZIaEa5hR5MPiD2GvpM2DIpMpgw2GjqGhmC6PgmyRKeDnC8HZRgiDApOUmWEsJwsPYzoLX1jSlwnkNZlF16PR14/XMEQxojOuoTCFMSM4QDBAtL4gUg5hDY2Eye4RDCbBQIJlSifb6/BgZI4gLY7HJAA===");
});
});

it("should return hash for version 2 view definition", () => {
const body = {
dataCubeName: "wiki",
viewDefinitionVersion: "2",
viewDefinition: ViewDefinitionConverter2Fixtures.fullTable()
};

cy.request("POST", "http://localhost:9090/mkurl", body).then((hash: Cypress.Response<MkurlResponse>) => {
expect(hash.status).to.eq(200);
expect(hash.body.hash).to.eq("#wiki/4/N4IgbglgzgrghgGwgLzgFwgewHYgFwhpwBGCApiADTjTxKoY4DKZaG2A5lPqAMaYIEcAA5QyAJUwB3bngBmiMQF9qGALZlkOCgQCiaXgHoAqgBUAwlRByICNGQBOsgNqg0AT2E7CEDVYdkcvg+fqq+EnCcZC6gUEQOaMEATAAMAIwArAC0KQCcWWlJpikpeCVlKQB0JSkAWlZk2AAmyenZeQUAzMWl5SXVJfVKALoqbp7ecQ4QnP6BwbwAFpHYZAhWcLyMuAQzVmCIMNH4ziCNIMPU2JiJ8opkY4QTwcSYAmSRc0G7UJKvidQDggji40A4jpcQNdbmCjo8PF5glMZhwvsFhHAOBRqJttgscEQZtxAYdjnhTgApRxkNTuC5XG74BQIZSUcaIgjI2bUALfEAwMQOczLJwbLZYHYgNToJb7UkuEAAPQAggASelQxl3FkPNlPDlQmBqYiONEEfhqDTYNAAGUaHDQiys0KZ9x5kSxMRAcTgCXwnWojRaeGwMEE1FeMGashAzgAlCARiNqFBhEg0F6EZMwSirE1wtgoBKFstsKt1inMH68KBecEmmsiFYs0jHBBjtR8wFxTh69FeEHc9QvNNMMGQInqEg1BBbhkUvDngRXu9Pp2C0Xez8/ozK9Xa/MCA27HBm0vvW2OyAu2Qe5KG1AB80hyAR1hx5OQNPZ/gMouDaGxqmuuVqbpKFpWra9qOlYHAOJEYa+rOdJ4GkKR7rcB58seTaqOegrtsS14QN2eJHv2g7cq+bZjsEn7fnO/7eOo2LEaBxYECxsHwaGQjTB4wQAAqmGkAASVhQFWmEgHW5EnmeBoEVeN53n2j6Uaiw40R+Khfr4P4hmGCDJhe0xkq4MmBNS2ADn28nUHIVbStJLbkQoYYAuApJ0bprlSh8sABJOWHWbZ5qYFGnmOQ4zk8Pq3gNu5dhysC3ifn5GhwIFFCPLyoXeHATQNi0DlOegcV+YlcAeSlRw+XhBqZdliaQsIMyrE0AAiG4SgqSwrGszpwBoqabKx0A7okrXtWQTRMFJdlNkoQA=");
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { OauthMessageView } from "../../oauth/oauth-message-view";
import { Ajax } from "../../utils/ajax/ajax";
import { reportError } from "../../utils/error-reporter/error-reporter";
import { replaceHash } from "../../utils/url/url";
import { ApiContext, CreateApiContext } from "../../views/cube-view/api-context";
import { CubeView } from "../../views/cube-view/cube-view";
import { SettingsContext, SettingsContextValue } from "../../views/cube-view/settings-context";
import { GeneralError } from "../../views/error-view/general-error";
Expand Down Expand Up @@ -241,10 +242,12 @@ export class TurniloApplication extends React.Component<TurniloApplicationProps,
return <React.StrictMode>
<main className="turnilo-application">
<SettingsContext.Provider value={this.getSettingsContext()}>
{this.renderView()}
{this.renderAboutModal()}
<Notifications/>
<Questions/>
<CreateApiContext appSettings={this.props.appSettings}>
{this.renderView()}
{this.renderAboutModal()}
<Notifications/>
<Questions/>
</CreateApiContext>
</SettingsContext.Provider>
</main>
</React.StrictMode>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
*/

import { Set } from "immutable";
import { $, Dataset } from "plywood";
import { Dataset } from "plywood";
import React from "react";
import { Dimension } from "../../../../common/models/dimension/dimension";
import { Essence } from "../../../../common/models/essence/essence";
import { BooleanFilterClause, FilterClause } from "../../../../common/models/filter-clause/filter-clause";
import { Stage } from "../../../../common/models/stage/stage";
import { Timekeeper } from "../../../../common/models/timekeeper/timekeeper";
import { Unary } from "../../../../common/utils/functional/functional";
import { Fn } from "../../../../common/utils/general/general";
import { STRINGS } from "../../../config/constants";
import { ApiContext, ApiContextValue } from "../../../views/cube-view/api-context";
import { BubbleMenu } from "../../bubble-menu/bubble-menu";
import { Button } from "../../button/button";
import { Checkbox } from "../../checkbox/checkbox";
Expand All @@ -36,7 +36,6 @@ interface BooleanFilterMenuProps {
dimension: Dimension;
saveClause: Unary<FilterClause, void>;
essence: Essence;
timekeeper: Timekeeper;
onClose: Fn;
containerStage?: Stage;
openOn: Element;
Expand All @@ -52,6 +51,9 @@ interface BooleanFilterMenuState {
}

export class BooleanFilterMenu extends React.Component<BooleanFilterMenuProps, BooleanFilterMenuState> {
static contextType = ApiContext;

context: ApiContextValue;

state = this.initialValues();

Expand All @@ -72,16 +74,11 @@ export class BooleanFilterMenu extends React.Component<BooleanFilterMenuProps, B
}

fetchData() {
const { essence, timekeeper, dimension } = this.props;
const { dataCube } = essence;
const filterExpression = essence.getEffectiveFilter(timekeeper, { unfilterDimension: dimension }).toExpression(dataCube);

const query = $("main")
.filter(filterExpression)
.split(dimension.expression, dimension.name);
const { booleanFilterQuery } = this.context;
const { essence, dimension } = this.props;

this.setState({ loading: true });
dataCube.executor(query, { timezone: essence.timezone })
booleanFilterQuery(essence, dimension)
.then(
(dataset: Dataset) => {
this.setState({
Expand Down
20 changes: 11 additions & 9 deletions src/client/modals/raw-data-modal/raw-data-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@

import { isDate } from "chronoshift";
import { List } from "immutable";
import { $, AttributeInfo, Dataset, Datum, Expression } from "plywood";
import { AttributeInfo, Dataset, Datum, Expression } from "plywood";
import React from "react";
import { ClientDataCube } from "../../../common/models/data-cube/data-cube";
import { findDimensionByName } from "../../../common/models/dimension/dimensions";
import { Essence } from "../../../common/models/essence/essence";
import { Locale } from "../../../common/models/locale/locale";
import { LIMIT } from "../../../common/models/raw-data-modal/raw-data-modal";
import { Stage } from "../../../common/models/stage/stage";
import { Timekeeper } from "../../../common/models/timekeeper/timekeeper";
import { formatFilterClause } from "../../../common/utils/formatter/formatter";
Expand All @@ -36,11 +37,11 @@ import { exportOptions, STRINGS } from "../../config/constants";
import { classNames } from "../../utils/dom/dom";
import { download, FileFormat, fileNameBase } from "../../utils/download/download";
import { getVisibleSegments } from "../../utils/sizing/sizing";
import { ApiContext, ApiContextValue } from "../../views/cube-view/api-context";
import "./raw-data-modal.scss";

const HEADER_HEIGHT = 30;
const ROW_HEIGHT = 30;
const LIMIT = 100;
const TIME_COL_WIDTH = 180;
const BOOLEAN_COL_WIDTH = 100;
const NUMBER_COL_WIDTH = 100;
Expand Down Expand Up @@ -83,7 +84,10 @@ function classFromAttribute(attribute: AttributeInfo): string {
}

export class RawDataModal extends React.Component<RawDataModalProps, RawDataModalState> {
static contextType = ApiContext;

public mounted: boolean;
context: ApiContextValue;

constructor(props: RawDataModalProps) {
super(props);
Expand All @@ -100,20 +104,18 @@ export class RawDataModal extends React.Component<RawDataModalProps, RawDataModa

componentDidMount() {
this.mounted = true;
const { essence, timekeeper } = this.props;
this.fetchData(essence, timekeeper);
const { essence } = this.props;
this.fetchData(essence);
}

componentWillUnmount() {
this.mounted = false;
}

fetchData(essence: Essence, timekeeper: Timekeeper): void {
const { dataCube } = essence;
const $main = $("main");
const query = $main.filter(essence.getEffectiveFilter(timekeeper).toExpression(dataCube)).limit(LIMIT);
fetchData(essence: Essence): void {
const { rawDataQuery } = this.context;
this.setState({ loading: true });
dataCube.executor(query, { timezone: essence.timezone })
rawDataQuery(essence)
.then(
(dataset: Dataset) => {
if (!this.mounted) return;
Expand Down
120 changes: 120 additions & 0 deletions src/client/views/cube-view/api-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2017-2022 Allegro.pl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Dataset, DatasetJS } from "plywood";
import React, { useContext } from "react";
import { ClientAppSettings } from "../../../common/models/app-settings/app-settings";
import { Dimension } from "../../../common/models/dimension/dimension";
import { Essence } from "../../../common/models/essence/essence";
import { Binary, Nullary, Unary } from "../../../common/utils/functional/functional";
import { DEFAULT_VIEW_DEFINITION_VERSION, definitionConverters } from "../../../common/view-definitions";
import { Ajax } from "../../utils/ajax/ajax";

export type VisualizationQuery = Unary<Essence, Promise<Dataset>>;

export type RawDataQuery = Unary<Essence, Promise<Dataset>>;

export type BooleanFilterQuery = Binary<Essence, Dimension, Promise<Dataset>>;

export interface ApiContextValue {
visualizationQuery: VisualizationQuery;
booleanFilterQuery: BooleanFilterQuery;
rawDataQuery: RawDataQuery;
}

export const ApiContext = React.createContext<ApiContextValue>({
get booleanFilterQuery(): BooleanFilterQuery {
throw new Error("Attempted to consume ApiContext when there was no Provider");
},
get visualizationQuery(): VisualizationQuery {
throw new Error("Attempted to consume ApiContext when there was no Provider");
},
get rawDataQuery(): RawDataQuery {
throw new Error("Attempted to consume ApiContext when there was no Provider");
}
});

export function useApiContext(): ApiContextValue {
return useContext(ApiContext);
}

interface QueryResponse {
result: DatasetJS;
}

type QueryEndpoints = "visualization" | "boolean-filter" | "raw-data";

type ExtraParams = Record<string, unknown>;

type SerializeExtraBase = (...args: any[]) => ExtraParams;
type QueryFunction<T extends SerializeExtraBase> = (essence: Essence, ...args: Parameters<T>) => Promise<Dataset>;

const emptyParams: Nullary<ExtraParams> = () => ({});

function createApiCall<T extends SerializeExtraBase>(settings: ClientAppSettings, query: QueryEndpoints, serializeExtraParams: T): QueryFunction<T> {
const { oauth, clientTimeout: timeout } = settings;
const viewDefinitionVersion = DEFAULT_VIEW_DEFINITION_VERSION;
const converter = definitionConverters[viewDefinitionVersion];
return (essence: Essence, ...args: Parameters<T>) => {
const extra = serializeExtraParams(...args);
const { dataCube: { name } } = essence;
const viewDefinition = converter.toViewDefinition(essence);
return Ajax.query<QueryResponse>({
method: "POST",
url: `query/${query}`,
timeout,
data: {
viewDefinitionVersion,
dataCube: name,
viewDefinition,
...extra
}
}, oauth).then(constructDataset);
};
}

const constructDataset = (res: QueryResponse) => Dataset.fromJS(res.result);

function createVizQueryApi(settings: ClientAppSettings): VisualizationQuery {
return createApiCall(settings, "visualization", emptyParams);
}

function createBooleanFilterQuery(settings: ClientAppSettings): BooleanFilterQuery {
return createApiCall(settings, "boolean-filter", (dimension: Dimension) => ({ dimension: dimension.name }));
}

function createRawDataQueryApi(settings: ClientAppSettings): RawDataQuery {
return createApiCall(settings, "raw-data", emptyParams);
}

function createApi(settings: ClientAppSettings): ApiContextValue {
return {
booleanFilterQuery: createBooleanFilterQuery(settings),
visualizationQuery: createVizQueryApi(settings),
rawDataQuery: createRawDataQueryApi(settings)
};
}

interface ApiContextProviderProps {
appSettings: ClientAppSettings;
}

export const CreateApiContext: React.FunctionComponent<ApiContextProviderProps> = ({ children, appSettings }) => {
const value = createApi(appSettings);
return <ApiContext.Provider value={value}>
{children}
</ApiContext.Provider>;
};
Loading