From 5995beb48b9efd11bb3d62e920f0584a60c5cd70 Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Thu, 1 Oct 2020 14:37:24 +0530 Subject: [PATCH 1/9] Update issue templates --- .../---documentation-improvement.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/---documentation-improvement.md diff --git a/.github/ISSUE_TEMPLATE/---documentation-improvement.md b/.github/ISSUE_TEMPLATE/---documentation-improvement.md new file mode 100644 index 0000000000..99f16d505f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---documentation-improvement.md @@ -0,0 +1,20 @@ +--- +name: "\U0001F4D6 Documentation Improvement" +about: Suggest improvements to our documentation +title: "[Docs] " +labels: Documentation +assignees: Nikhil-Nandagopal + +--- + +## Documentation Link + +Add a link to the page which needs improvement (if relevant) + +## Describe the problem + +Is the documentation missing? Or is it confusing? Why is it confusing? + +## Describe the improvement + +A clear and concise description of the improvement. From ae5929886623719f270fc75dcab37a9f0bd452e2 Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Thu, 1 Oct 2020 15:48:50 +0530 Subject: [PATCH 2/9] Release (#850) * Stop showing empty table when there are no records (#790) * Datasource explorer fixes (#789) - Expand datasource by default - Let column/key name take more space. - Show column popup on hover instead of click * Show datasources without grouping by plugins in explorer (#660) * Immutable Widgets (#799) Co-authored-by: Abhinav Jha * Removing the docker login step that causes forked PRs to fail (#844) Also adding pull_request_target event for release and master branch. This will run the GH Actions in the context of the base repository instead of the forked one. Hence, the secrets for Cypress, Docker etc can be passed down to the action for PRs being raised via a fork * Datasource structure support for MongoDB (#641) * Added structure for MongoDB datasources * Fixed tests for MongoDB template queries * Updating readme to test forked PRs (#845) * Adding additional docs contribution files. (#849) * Update CONTRIBUTING.md * Rename action.md to ActionsGuide.md * Rename asset-upload.md to UploadingAssets.md * Rename db-integration.md to DB Integrations.md * Rename widget.md to Widgets.md * Rename ActionsGuide.md to Actions.md * Create CONTRIBUTING.md * Update Actions.md * Update Actions.md * Update Widgets.md * Update UploadingAssets.md * Update CONTRIBUTING.md * Update CONTRIBUTING.md * Update CONTRIBUTING.md * Update CONTRIBUTING.md Co-authored-by: akash-codemonk <67054171+akash-codemonk@users.noreply.github.com> Co-authored-by: Hetu Nandu Co-authored-by: Abhinav Jha Co-authored-by: Arpit Mohan Co-authored-by: Shrikant Sharat Kandula Co-authored-by: satbir121 <39981226+satbir121@users.noreply.github.com> --- .github/workflows/client.yml | 3 +- CONTRIBUTING.md | 4 +- app/client/README.md | 2 +- ...xplorer_CopyQuery_RenameDatasource_spec.js | 1 - .../Entity_Explorer_Query_Datasource_spec.js | 1 - .../QueryPane/MongoDatasource_spec.js | 1 - .../QueryPane/PostgreDatasource_spec.js | 1 - app/client/cypress/support/commands.js | 1 - app/client/src/actions/pageActions.tsx | 4 +- .../src/entities/DataTree/dataTreeFactory.ts | 2 +- .../Explorer/Datasources/DatasourceEntity.tsx | 7 +- .../Explorer/Datasources/DatasourceField.tsx | 32 +-- .../Explorer/Datasources/DatasourcesGroup.tsx | 49 ++--- .../Explorer/Datasources/PluginGroup.tsx | 48 ----- .../src/pages/Editor/QueryEditor/Table.tsx | 38 +--- .../entityReducers/canvasWidgetsReducer.tsx | 18 +- app/client/src/sagas/ErrorSagas.tsx | 4 +- app/client/src/sagas/WidgetOperationSagas.tsx | 180 +++++++++++----- app/client/src/utils/WidgetPropsUtils.tsx | 1 - .../external/models/DatasourceStructure.java | 11 +- .../com/external/plugins/MongoPlugin.java | 202 ++++++++++++++++-- .../com/external/plugins/MongoPluginTest.java | 99 ++++++++- .../com/external/plugins/MySqlPlugin.java | 8 +- contributions/docs/Actions.md | 27 +++ contributions/docs/CONTRIBUTING.md | 34 +++ contributions/docs/DB Integrations.md | 21 ++ contributions/docs/UploadingAssets.md | 18 ++ contributions/docs/Widgets.md | 32 +++ 28 files changed, 620 insertions(+), 229 deletions(-) delete mode 100644 app/client/src/pages/Editor/Explorer/Datasources/PluginGroup.tsx create mode 100644 contributions/docs/Actions.md create mode 100644 contributions/docs/CONTRIBUTING.md create mode 100644 contributions/docs/DB Integrations.md create mode 100644 contributions/docs/UploadingAssets.md create mode 100644 contributions/docs/Widgets.md diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 6987b623e0..4f3f0f9759 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -6,7 +6,7 @@ on: # Only trigger if files have changed in this specific path paths: - 'app/client/**' - pull_request: + pull_request_target: branches: [ release, master ] paths: - 'app/client/**' @@ -131,7 +131,6 @@ jobs: - name: Pull server docker container and start it locally shell: bash run: | - echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin docker run -d --net=host --name appsmith-internal-server -p 8080:8080 \ --env APPSMITH_MONGODB_URI=mongodb://localhost:27017/appsmith \ --env APPSMITH_REDIS_URL=redis://localhost:6379 \ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb1bb8e7d7..0b45c1e31e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,10 +21,10 @@ We welcome all feature requests, whether it's to add new functionality to an exi File your feature request through GitHub Issues using the [Feature Request](https://github.com/appsmithorg/appsmith/issues/new?assignees=Nikhil-Nandagopal&labels=&template=----feature-request.md&title=%5BFeature%5D) template. #### 📝 Improve the documentation -In the process of shipping features quickly, we often forget to keep our docs up to date. You can help by suggesting improvements to our documentation or helping with one of our many [documentation issues](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3ADocumentation). Read our [Documentation Contribution Guide]() to get started! +In the process of shipping features quickly, we often forget to keep our docs up to date. You can help by suggesting improvements to our documentation or dive right in to our [Contribution Guide](contributions/docs/CONTRIBUTING.md)! #### ⚙️ Close a Bug / Feature issue -We welcome contributions that help make appsmith bug free & improve the experience of our users. Check our [good first issues](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22) & [begin contributing](#code-contribution) +We welcome contributions that help make appsmith bug free & improve the experience of our users. Check out the [Contribution Section](#code-contribution) below to begin. ## Code Contribution All Code contributions are welcome and highly encouraged. Before opening a PR, create an issue and talk to a maintainer about a possible solution. diff --git a/app/client/README.md b/app/client/README.md index 9521fb10f7..055787f8ff 100755 --- a/app/client/README.md +++ b/app/client/README.md @@ -82,4 +82,4 @@ This section has moved here: https://facebook.github.io/create-react-app/docs/tr ### Cypress tests via Github Actions -The cypress tests run via Github Actions. The Github Action pulls the `release` Docker image of the server to run as a service locally. This is to ensure that we don't face any network flakiness during tests. \ No newline at end of file +Cypress is our integration test framework of choice. The Cypress tests run via Github Actions. The Github Action `.github/workflows/client.yml` pulls the `release` Docker image of the server to run as a service locally. This is to ensure that we don't face any network flakiness during tests. diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js index 1e020b7499..39a8fc5798 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js @@ -77,7 +77,6 @@ describe("Entity explorer tests related to copy query", function() { cy.deleteQuery(); cy.get(commonlocators.entityExplorersearch).clear(); cy.NavigateToDatasourceEditor(); - cy.get(datasource.PostgresEntity).click(); cy.GlobalSearchEntity(`${datasourceName}`); cy.get(`.t--entity-name:contains(${datasourceName})`).click(); cy.generateUUID().then(uid => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js index 1a34b9114a..99b303f344 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js @@ -65,7 +65,6 @@ describe("Entity explorer tests related to query and datasource", function() { cy.deleteQuery(); cy.get(commonlocators.entityExplorersearch).clear(); cy.NavigateToDatasourceEditor(); - cy.get(datasource.PostgresEntity).click(); cy.get("@createDatasource").then(httpResponse => { const datasourceName = httpResponse.response.body.data.name; cy.GlobalSearchEntity(`${datasourceName}`); diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js index 85261492f6..dee40069e2 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js @@ -39,7 +39,6 @@ describe("Create a query with a mongo datasource, run, save and then delete the cy.runAndDeleteQuery(); cy.NavigateToDatasourceEditor(); - cy.get(".t--entity-name:contains(MongoDB)").click(); cy.get("@createDatasource").then(httpResponse => { const datasourceName = httpResponse.response.body.data.name; diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js index 83f696674d..15f80ed9aa 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js @@ -49,7 +49,6 @@ describe("Create a query with a postgres datasource, run, save and then delete t }); it("Deletes a datasource", () => { cy.NavigateToDatasourceEditor(); - cy.get(".t--entity-name:contains(PostgreSQL)").click(); cy.get(`.t--entity-name:contains(${datasourceName})`).click(); cy.get(".t--delete-datasource").click(); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 33941a3bd5..8d44458704 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -1340,7 +1340,6 @@ Cypress.Commands.add("createPostgresDatasource", () => { Cypress.Commands.add("deletePostgresDatasource", datasourceName => { cy.NavigateToDatasourceEditor(); - cy.get(".t--entity-name:contains(PostgreSQL)").click(); cy.get(`.t--entity-name:contains(${datasourceName})`).click(); cy.get(".t--delete-datasource").click(); diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index acecde88ec..691e45ed45 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -8,7 +8,7 @@ import { SavePageSuccessPayload, FetchPageListPayload, } from "constants/ReduxActionConstants"; -import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; +import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { ContainerWidgetProps } from "widgets/ContainerWidget"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { APP_MODE, UrlDataState } from "reducers/entityReducers/appReducer"; @@ -85,7 +85,7 @@ export const deletePageSuccess = () => { }; }; -export const updateAndSaveLayout = (widgets: FlattenedWidgetProps) => { +export const updateAndSaveLayout = (widgets: CanvasWidgetsReduxState) => { return { type: ReduxActionTypes.UPDATE_LAYOUT, payload: { widgets }, diff --git a/app/client/src/entities/DataTree/dataTreeFactory.ts b/app/client/src/entities/DataTree/dataTreeFactory.ts index f629b8cfb9..a32f8a6b5b 100644 --- a/app/client/src/entities/DataTree/dataTreeFactory.ts +++ b/app/client/src/entities/DataTree/dataTreeFactory.ts @@ -139,7 +139,7 @@ export class DataTreeFactory { widget.type, ); const derivedProps: any = {}; - const dynamicBindings = widget.dynamicBindings || {}; + const dynamicBindings = { ...widget.dynamicBindings } || {}; Object.keys(dynamicBindings).forEach(propertyName => { if (_.isObject(widget[propertyName])) { // Stringify this because composite controls may have bindings in the sub controls diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx index 0f8ca0705a..a9fa4f293f 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx @@ -1,7 +1,8 @@ import React, { useCallback } from "react"; import { Datasource } from "api/DatasourcesApi"; +import { Plugin } from "api/PluginApi"; import DataSourceContextMenu from "./DataSourceContextMenu"; -import { queryIcon } from "../ExplorerIcons"; +import { getPluginIcon } from "../ExplorerIcons"; import { useParams } from "react-router"; import { ExplorerURLParams, getDatasourceIdFromURL } from "../helpers"; import Entity, { EntityClassNames } from "../Entity"; @@ -16,6 +17,7 @@ import { AppState } from "reducers"; import { DatasourceStructureContainer } from "./DatasourceStructureContainer"; type ExplorerDatasourceEntityProps = { + plugin: Plugin; datasource: Datasource; step: number; searchKeyword?: string; @@ -26,6 +28,7 @@ export const ExplorerDatasourceEntity = ( ) => { const params = useParams(); const dispatch = useDispatch(); + const icon = getPluginIcon(props.plugin); const switchDatasource = useCallback( () => history.push( @@ -58,7 +61,7 @@ export const ExplorerDatasourceEntity = ( entityId={props.datasource.id} className="datasource" key={props.datasource.id} - icon={queryIcon} + icon={icon} name={props.datasource.name} active={active} step={props.step} diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceField.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceField.tsx index 85b9131dc8..67c5a7f373 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceField.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceField.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Popover, Position } from "@blueprintjs/core"; +import { Popover, Position, PopoverInteractionKind } from "@blueprintjs/core"; import { DATASOURCE_FIELD_ICONS_MAP, datasourceColumnIcon, @@ -22,7 +22,7 @@ const Wrapper = styled.div<{ step: number }>` cursor: pointer; `; -const Value = styled.div` +const FieldName = styled.div` color: ${Colors.ALTO}; flex: 1; font-size: 12px; @@ -30,13 +30,15 @@ const Value = styled.div` overflow: hidden; line-height: 13px; text-overflow: ellipsis; - :nth-child(2) { - text-align: right; - font-size: 10px; - line-height: 12px; - color: #777777; - font-weight: 300; - } + padding-right: 30px; +`; + +const FieldValue = styled.div` + text-align: right; + font-size: 10px; + line-height: 12px; + color: #777777; + font-weight: 300; `; const Content = styled.div` @@ -93,14 +95,20 @@ export const DatabaseColumns = (props: DatabaseFieldProps) => { {icon} - {fieldName} - {fieldType} + {fieldName} + {fieldType} ); return ( - + {content} {icon} diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourcesGroup.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourcesGroup.tsx index bc3af46367..14be4e4bb2 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourcesGroup.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourcesGroup.tsx @@ -1,16 +1,16 @@ -import React, { useMemo, ReactNode } from "react"; +import React, { useMemo } from "react"; import { datasourceIcon } from "../ExplorerIcons"; import Entity from "../Entity"; -import { groupBy } from "lodash"; +import { keyBy } from "lodash"; import { DATA_SOURCES_EDITOR_URL } from "constants/routes"; import { useParams } from "react-router"; import { ExplorerURLParams } from "../helpers"; import history from "utils/history"; -import { Plugin } from "api/PluginApi"; -import DatasourcePluginGroup from "./PluginGroup"; + import { useSelector } from "react-redux"; import { AppState } from "reducers"; import { Datasource } from "api/DatasourcesApi"; +import ExplorerDatasourceEntity from "./DatasourceEntity"; type ExplorerDatasourcesGroupProps = { step: number; @@ -25,29 +25,12 @@ export const ExplorerDatasourcesGroup = ( const plugins = useSelector((state: AppState) => { return state.entities.plugins.list; }); - const { datasources } = props; + const { datasources = [] } = props; const disableDatasourceGroup = !datasources || !datasources.length; - const pluginGroups = useMemo(() => groupBy(datasources, "pluginId"), [ - datasources, - ]); + const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]); - const pluginGroupNodes: ReactNode[] = []; - for (const [pluginId, datasources] of Object.entries(pluginGroups)) { - const plugin = plugins.find((plugin: Plugin) => plugin.id === pluginId); - - pluginGroupNodes.push( - , - ); - } - - if (pluginGroupNodes.length === 0 && props.searchKeyword) return null; + if (disableDatasourceGroup && props.searchKeyword) return null; return ( -1 } - isDefaultExpanded={ - window.location.pathname.indexOf( - DATA_SOURCES_EDITOR_URL(params.applicationId, params.pageId), - ) > -1 || !!props.searchKeyword - } + isDefaultExpanded disabled={disableDatasourceGroup} onCreate={() => { history.push( @@ -72,7 +51,17 @@ export const ExplorerDatasourcesGroup = ( ); }} > - {pluginGroupNodes} + {datasources.map((datasource: Datasource) => { + return ( + + ); + })} ); }; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/PluginGroup.tsx b/app/client/src/pages/Editor/Explorer/Datasources/PluginGroup.tsx deleted file mode 100644 index 39445574d5..0000000000 --- a/app/client/src/pages/Editor/Explorer/Datasources/PluginGroup.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from "react"; -import Entity from "../Entity"; -import { Plugin } from "api/PluginApi"; -import { Datasource } from "api/DatasourcesApi"; -import { getPluginIcon } from "../ExplorerIcons"; -import { getDatasourceIdFromURL } from "../helpers"; -import ExplorerDatasourceEntity from "./DatasourceEntity"; - -type DatasourcePluginGroupProps = { - plugin?: Plugin; - datasources: Datasource[]; - step: number; - searchKeyword?: string; -}; -export const DatasourcePluginGroup = (props: DatasourcePluginGroupProps) => { - const pluginIcon = getPluginIcon(props.plugin); - const datasourceIdFromURL = getDatasourceIdFromURL(); - const currentGroup = - !!datasourceIdFromURL && - props.datasources - .map((datasource: Datasource) => datasource.id) - .indexOf(datasourceIdFromURL) > -1; - - return ( - - {props.datasources.map((datasource: Datasource) => { - return ( - - ); - })} - - ); -}; - -export default DatasourcePluginGroup; diff --git a/app/client/src/pages/Editor/QueryEditor/Table.tsx b/app/client/src/pages/Editor/QueryEditor/Table.tsx index 9dc40037f5..b203296947 100644 --- a/app/client/src/pages/Editor/QueryEditor/Table.tsx +++ b/app/client/src/pages/Editor/QueryEditor/Table.tsx @@ -95,6 +95,8 @@ const Table = (props: TableProps) => { useFlexLayout, ); + if (rows.length === 0 || headerGroups.length === 0) return null; + return ( { ))} ))} - {headerGroups.length === 0 && renderEmptyRows(1, 2)}
{rows.map((row: any, index: number) => { prepareRow(row); @@ -150,7 +151,6 @@ const Table = (props: TableProps) => {
); })} - {rows.length === 0 && renderEmptyRows(1, 2)} @@ -158,38 +158,4 @@ const Table = (props: TableProps) => { ); }; -const renderEmptyRows = (rowCount: number, columns: number) => { - const rows: string[] = new Array(rowCount).fill(""); - const tableColumns = new Array(columns).fill(""); - return ( - - {rows.map((row: string, index: number) => { - return ( -
- {tableColumns.map((column: any, colIndex: number) => { - return ( -
- ); - })} -
- ); - })} - - ); -}; - export default Table; diff --git a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx index 0066393c28..2588ca7dcc 100644 --- a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx +++ b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx @@ -1,4 +1,4 @@ -import { createReducer } from "utils/AppsmithUtils"; +import { createImmerReducer } from "utils/AppsmithUtils"; import { ReduxActionTypes, UpdateCanvasPayload, @@ -13,31 +13,25 @@ export type FlattenedWidgetProps = WidgetProps & { children?: string[]; }; -const canvasWidgetsReducer = createReducer(initialState, { +const canvasWidgetsReducer = createImmerReducer(initialState, { [ReduxActionTypes.UPDATE_CANVAS]: ( state: CanvasWidgetsReduxState, action: ReduxAction, ) => { - return { ...action.payload.widgets }; + return action.payload.widgets; }, [ReduxActionTypes.UPDATE_LAYOUT]: ( state: CanvasWidgetsReduxState, action: ReduxAction, ) => { - return { ...action.payload.widgets }; + return action.payload.widgets; }, [ReduxActionTypes.UPDATE_WIDGET_PROPERTY]: ( state: CanvasWidgetsReduxState, action: ReduxAction, ) => { - const widget = state[action.payload.widgetId]; - return { - ...state, - [action.payload.widgetId]: { - ...widget, - [action.payload.propertyName]: action.payload.propertyValue, - }, - }; + state[action.payload.widgetId][action.payload.propertyName] = + action.payload.propertyValue; }, }); diff --git a/app/client/src/sagas/ErrorSagas.tsx b/app/client/src/sagas/ErrorSagas.tsx index b823abee67..c268d8f5bc 100644 --- a/app/client/src/sagas/ErrorSagas.tsx +++ b/app/client/src/sagas/ErrorSagas.tsx @@ -10,6 +10,7 @@ import { ApiResponse } from "api/ApiResponses"; import { put, takeLatest, call } from "redux-saga/effects"; import { ERROR_401, ERROR_500, ERROR_0 } from "constants/messages"; import { ToastType } from "react-toastify"; +import log from "loglevel"; export function* callAPI(apiCall: any, requestPayload: any) { try { @@ -84,7 +85,8 @@ export function* errorSaga( ) { // Just a pass through for now. // Add procedures to customize errors here - console.log({ error: errorAction }); + log.debug(`Error in action ${errorAction.type}`); + log.error(errorAction.payload.error); // Show a toast when the error occurs const { type, diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index d7e8650d4f..c31fd35d58 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -158,26 +158,32 @@ function* generateChildWidgets( export function* addChildSaga(addChildAction: ReduxAction) { try { + const start = performance.now(); AppToaster.clear(); const { widgetId } = addChildAction.payload; // Get the current parent widget whose child will be the new widget. - const parent: FlattenedWidgetProps = yield select(getWidget, widgetId); + const stateParent: FlattenedWidgetProps = yield select(getWidget, widgetId); + // const parent = Object.assign({}, stateParent); // Get all the widgets from the canvasWidgetsReducer - const widgets = yield select(getWidgets); + const stateWidgets = yield select(getWidgets); + const widgets = Object.assign({}, stateWidgets); // Generate the full WidgetProps of the widget to be added. const childWidgetPayload: GeneratedWidgetPayload = yield generateChildWidgets( - parent, + stateParent, addChildAction.payload, widgets, ); // Update widgets to put back in the canvasWidgetsReducer // TODO(abhinav): This won't work if dont already have an empty children: [] - - if (parent.children) parent.children.push(childWidgetPayload.widgetId); + const parent = { + ...stateParent, + children: [...stateParent.children, childWidgetPayload.widgetId], + }; widgets[parent.widgetId] = parent; + log.debug("add child computations took", performance.now() - start, "ms"); yield put(updateAndSaveLayout(widgets)); } catch (error) { yield put({ @@ -199,7 +205,8 @@ export function* addChildrenSaga( ) { try { const { widgetId, children } = addChildrenAction.payload; - const widgets = yield select(getWidgets); + const stateWidgets = yield select(getWidgets); + const widgets = { ...stateWidgets }; const widgetNames = Object.keys(widgets).map(w => widgets[w].widgetName); children.forEach(child => { @@ -215,12 +222,13 @@ export function* addChildrenSaga( widgetName: newWidgetName, renderMode: RenderModes.CANVAS, }; - if ( - widgets[widgetId].children && - Array.isArray(widgets[widgetId].children) - ) { - widgets[widgetId].children?.push(child.widgetId); - } else widgets[widgetId].children = [child.widgetId]; + + const existingChildren = widgets[widgetId].children || []; + + widgets[widgetId] = { + ...widgets[widgetId], + children: [...existingChildren, child.widgetId], + }; } }); @@ -265,9 +273,15 @@ export function* deleteSaga(deleteAction: ReduxAction) { } if (widgetId && parentId) { - const widgets = yield select(getWidgets); - const widget = yield select(getWidget, widgetId); - const parent: FlattenedWidgetProps = yield select(getWidget, parentId); + const stateWidgets = yield select(getWidgets); + const widgets = { ...stateWidgets }; + const stateWidget = yield select(getWidget, widgetId); + const widget = { ...stateWidget }; + const stateParent: FlattenedWidgetProps = yield select( + getWidget, + parentId, + ); + let parent = { ...stateParent }; const analyticsEvent = isShortcut ? "WIDGET_DELETE_VIA_SHORTCUT" @@ -281,9 +295,10 @@ export function* deleteSaga(deleteAction: ReduxAction) { // Remove entry from parent's children if (parent.children) { - const indexOfChild = parent.children.indexOf(widgetId); - if (indexOfChild > -1) delete parent.children[indexOfChild]; - parent.children = parent.children.filter(Boolean); + parent = { + ...parent, + children: parent.children.filter(c => c !== widgetId), + }; } widgets[parentId] = parent; @@ -319,11 +334,12 @@ export function* deleteSaga(deleteAction: ReduxAction) { }, WIDGET_DELETE_UNDO_TIMEOUT); } - otherWidgetsToDelete.forEach(widget => { - delete widgets[widget.widgetId]; - }); + const finalWidgets = _.omit( + widgets, + otherWidgetsToDelete.map(widgets => widgets.widgetId), + ); - yield put(updateAndSaveLayout(widgets)); + yield put(updateAndSaveLayout(finalWidgets)); } } catch (error) { yield put({ @@ -337,13 +353,18 @@ export function* deleteSaga(deleteAction: ReduxAction) { } export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { + // Get the list of widget and its children which were deleted const deletedWidgets: FlattenedWidgetProps[] = yield getDeletedWidgets( action.payload.widgetId, ); + // Find the parent in the list of deleted widgets const deletedWidget = deletedWidgets.find( widget => widget.widgetId === action.payload.widgetId, ); + + // If the deleted widget is infact available. if (deletedWidget) { + // Log an undo event AnalyticsUtil.logEvent("WIDGET_DELETE_UNDO", { widgetName: deletedWidget.widgetName, widgetType: deletedWidget.type, @@ -351,13 +372,18 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { } if (deletedWidgets) { - const widgets = yield select(getWidgets); + // Get the current list of widgets from reducer + const stateWidgets = yield select(getWidgets); + let widgets = { ...stateWidgets }; + // For each deleted widget deletedWidgets.forEach(widget => { + // Add it to the widgets list we fetched from reducer widgets[widget.widgetId] = widget; + // If the widget in question is the deleted widget if (widget.widgetId === action.payload.widgetId) { //SPECIAL HANDLING FOR TAB IN A TABS WIDGET if (widget.tabId && widget.type === WidgetTypes.CANVAS_WIDGET) { - const parent = widgets[widget.parentId]; + const parent = { ...widgets[widget.parentId] }; if (parent.tabs) { try { const tabs = _.isString(parent.tabs) @@ -368,7 +394,13 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { widgetId: widget.widgetId, label: widget.tabName || widget.widgetName, }); - widgets[widget.parentId].tabs = JSON.stringify(tabs); + widgets = { + ...widgets, + [widget.parentId]: { + ...widgets[widget.parentId], + tabs: JSON.stringify(tabs), + }, + }; } catch (error) { log.debug("Error deleting tabs widget: ", { error }); } @@ -380,11 +412,24 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { label: widget.tabName || widget.widgetName, }, ]); + widgets = { + ...widgets, + [widget.parentId]: parent, + }; } } - if (widgets[widget.parentId].children) - widgets[widget.parentId].children?.push(widget.widgetId); - else widgets[widget.parentId].children = [widget.widgetId]; + let newChildren = [widget.widgetId]; + if (widgets[widget.parentId].children) { + // Concatenate the list of paren't children with the current widgetId + newChildren = newChildren.concat(widgets[widget.parentId].children); + } + widgets = { + ...widgets, + [widget.parentId]: { + ...widgets[widget.parentId], + children: newChildren, + }, + }; } }); @@ -396,7 +441,7 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { export function* moveSaga(moveAction: ReduxAction) { try { AppToaster.clear(); - + const start = performance.now(); const { widgetId, leftColumn, @@ -404,20 +449,25 @@ export function* moveSaga(moveAction: ReduxAction) { parentId, newParentId, } = moveAction.payload; - let widget: FlattenedWidgetProps = yield select(getWidget, widgetId); + const stateWidget: FlattenedWidgetProps = yield select(getWidget, widgetId); + let widget = Object.assign({}, stateWidget); // Get all widgets from DSL/Redux Store - const widgets = yield select(getWidgets) as any; + const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets); + const widgets = Object.assign({}, stateWidgets); // Get parent from DSL/Redux Store - const parent: FlattenedWidgetProps = yield select(getWidget, parentId); + const stateParent: FlattenedWidgetProps = yield select(getWidget, parentId); + const parent = { ...stateParent, children: [...stateParent.children] }; // Update position of widget - widget = updateWidgetPosition(widget, leftColumn, topRow); + const updatedPosition = updateWidgetPosition(widget, leftColumn, topRow); + widget = { ...widget, ...updatedPosition }; + // Replace widget with update widget props widgets[widgetId] = widget; // If the parent has changed i.e parentWidgetId is not parent.widgetId if (parent.widgetId !== newParentId && widgetId !== newParentId) { // Remove from the previous parent - if (parent.children) { + if (parent.children && Array.isArray(parent.children)) { const indexOfChild = parent.children.indexOf(widgetId); if (indexOfChild > -1) delete parent.children[indexOfChild]; parent.children = parent.children.filter(Boolean); @@ -426,16 +476,17 @@ export function* moveSaga(moveAction: ReduxAction) { // Add to new parent widgets[parent.widgetId] = parent; - if ( - widgets[newParentId].children && - Array.isArray(widgets[newParentId].children) - ) { - widgets[newParentId].children?.push(widgetId); - } else { - widgets[newParentId].children = [widgetId]; - } + const newParent = { + ...widgets[newParentId], + children: widgets[newParentId].children + ? [...widgets[newParentId].children, widgetId] + : [widgetId], + }; widgets[widgetId].parentId = newParentId; + widgets[newParentId] = newParent; } + log.debug("move computations took", performance.now() - start, "ms"); + yield put(updateAndSaveLayout(widgets)); } catch (error) { yield put({ @@ -451,7 +502,7 @@ export function* moveSaga(moveAction: ReduxAction) { export function* resizeSaga(resizeAction: ReduxAction) { try { AppToaster.clear(); - + const start = performance.now(); const { widgetId, leftColumn, @@ -460,11 +511,14 @@ export function* resizeSaga(resizeAction: ReduxAction) { bottomRow, } = resizeAction.payload; - let widget: FlattenedWidgetProps = yield select(getWidget, widgetId); - const widgets = yield select(getWidgets); + const stateWidget: FlattenedWidgetProps = yield select(getWidget, widgetId); + let widget = { ...stateWidget }; + const stateWidgets = yield select(getWidgets); + const widgets = { ...stateWidgets }; widget = { ...widget, leftColumn, rightColumn, topRow, bottomRow }; widgets[widgetId] = widget; + log.debug("resize computations took", performance.now() - start, "ms"); yield put(updateAndSaveLayout(widgets)); } catch (error) { yield put({ @@ -513,7 +567,8 @@ function* updateDynamicBindings( stringProp = JSON.stringify(propertyValue); } const isDynamic = isDynamicValue(stringProp); - let dynamicBindings: Record = widget.dynamicBindings || {}; + let dynamicBindings: Record = + { ...widget.dynamicBindings } || {}; if (!isDynamic && propertyName in dynamicBindings) { dynamicBindings = _.omit(dynamicBindings, propertyName); } @@ -531,7 +586,8 @@ function* updateWidgetPropertySaga( const { payload: { propertyValue, propertyName, widgetId }, } = updateAction; - const widget: WidgetProps = yield select(getWidget, widgetId); + const stateWidget: WidgetProps = yield select(getWidget, widgetId); + const widget = { ...stateWidget }; const dynamicTriggersUpdated = yield updateDynamicTriggers( widget, @@ -542,7 +598,8 @@ function* updateWidgetPropertySaga( yield updateDynamicBindings(widget, propertyName, propertyValue); yield put(updateWidgetProperty(widgetId, propertyName, propertyValue)); - const widgets = yield select(getWidgets); + const stateWidgets = yield select(getWidgets); + const widgets = { ...stateWidgets, [widgetId]: widget }; yield put(updateAndSaveLayout(widgets)); } @@ -623,7 +680,6 @@ function* updateCanvasSize( function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) { const selectedWidget = yield select(getSelectedWidget); - console.log({ selectedWidget }); if (!selectedWidget) return; const widgets = yield select(getWidgets); const widgetsToStore = getAllWidgetsInTree(selectedWidget.widgetId, widgets); @@ -696,7 +752,8 @@ function* pasteWidgetSaga() { widgetType: copiedWidget.type, }); - const widgets = yield select(getWidgets); + const stateWidgets = yield select(getWidgets); + let widgets = { ...stateWidgets }; const selectedWidget = yield select(getSelectedWidget); let newWidgetParentId = MAIN_CONTAINER_WIDGET_ID; @@ -808,14 +865,23 @@ function* pasteWidgetSaga() { widget.parentId = newWidgetParentId; // Also, update the parent widget in the canvas widgets // to include this new copied widget's id in the parent's children + let parentChildren = [widget.widgetId]; if ( widgets[newWidgetParentId].children && Array.isArray(widgets[newWidgetParentId].children) ) { - widgets[newWidgetParentId].children.push(widget.widgetId); - } else { - widgets[newWidgetParentId].children = [widget.widgetId]; + // Add the new child to existing children + parentChildren = parentChildren.concat( + widgets[newWidgetParentId].children, + ); } + widgets = { + ...widgets, + [newWidgetParentId]: { + ...widgets[newWidgetParentId], + children: parentChildren, + }, + }; // If the copied widget's boundaries exceed the parent's // Make the parent scrollable if ( @@ -824,9 +890,11 @@ function* pasteWidgetSaga() { widget.bottomRow * widget.parentRowSpace ) { if (widget.parentId !== MAIN_CONTAINER_WIDGET_ID) { - widgets[ - widgets[newWidgetParentId].parentId - ].shouldScrollContents = true; + const parent = widgets[widgets[newWidgetParentId].parentId]; + widgets[widgets[newWidgetParentId].parentId] = { + ...parent, + shouldScrollContents: true, + }; } } } else { diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index afaa13f133..792c5e3083 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -461,7 +461,6 @@ export const updateWidgetPosition = ( }; return { - ...widget, ...newPositions, }; }; diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStructure.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStructure.java index 9e43b64cfc..f2c5144060 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStructure.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStructure.java @@ -31,10 +31,19 @@ public class DatasourceStructure { @Data @AllArgsConstructor - public static class Column { + public static class Column implements Comparable { String name; String type; String defaultValue; + + @Override + public int compareTo(Column other) { + if (other == null || other.getName() == null) { + return 1; + } + + return name.compareTo(other.getName()); + } } public interface Key extends Comparable { diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java index d7e9cd7219..8c22861c49 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java @@ -5,6 +5,7 @@ import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.Connection; import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; import com.appsmith.external.models.SSLDetails; @@ -22,6 +23,8 @@ import com.mongodb.client.MongoDatabase; import lombok.extern.slf4j.Slf4j; import org.bson.Document; import org.bson.conversions.Bson; +import org.bson.types.Decimal128; +import org.bson.types.ObjectId; import org.json.JSONArray; import org.json.JSONObject; import org.pf4j.Extension; @@ -38,8 +41,12 @@ import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; +import java.util.Date; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -91,16 +98,7 @@ public class MongoPlugin extends BasePlugin { ActionExecutionResult result = new ActionExecutionResult(); - // Explicitly set default database. - String databaseName = datasourceConfiguration.getConnection().getDefaultDatabaseName(); - - // If that's not available, pick the authentication database. - final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); - if (StringUtils.isEmpty(databaseName) && authentication != null) { - databaseName = authentication.getDatabaseName(); - } - - MongoDatabase database = mongoClient.getDatabase(databaseName); + MongoDatabase database = mongoClient.getDatabase(getDatabaseName(datasourceConfiguration)); Bson command = Document.parse(actionConfiguration.getBody()); @@ -165,6 +163,19 @@ public class MongoPlugin extends BasePlugin { return Mono.just(result); } + private String getDatabaseName(DatasourceConfiguration datasourceConfiguration) { + // Explicitly set default database. + String databaseName = datasourceConfiguration.getConnection().getDefaultDatabaseName(); + + // If that's not available, pick the authentication database. + final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + if (StringUtils.isEmpty(databaseName) && authentication != null) { + databaseName = authentication.getDatabaseName(); + } + + return databaseName; + } + @Override public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { // TODO: ReadOnly seems to be not supported at the driver level. The recommendation is to connect with a @@ -328,7 +339,7 @@ public class MongoPlugin extends BasePlugin { log.warn("Timeout connecting to MongoDB from MongoPlugin.", e); return new DatasourceTestResult("Timed out trying to connect to MongoDB host."); - } catch(MongoCommandException e) { + } catch (MongoCommandException e) { // The fact that we got a response saying "Unauthorized" means that the connection to the // MongoDB instance is valid. It also means we don't have access to the admin database, but // that's okay for our purposes here. @@ -354,12 +365,177 @@ public class MongoPlugin extends BasePlugin { .onErrorResume(error -> Mono.just(new DatasourceTestResult(error.getMessage()))); } - private static String urlEncode(String text) { - return URLEncoder.encode(text, StandardCharsets.UTF_8); + @Override + public Mono getStructure(MongoClient mongoClient, DatasourceConfiguration datasourceConfiguration) { + final DatasourceStructure structure = new DatasourceStructure(); + List tables = new ArrayList<>(); + structure.setTables(tables); + + final MongoDatabase database = mongoClient.getDatabase(getDatabaseName(datasourceConfiguration)); + + for (Document collection : database.listCollections()) { + final String collectionName = collection.getString("name"); + + final ArrayList columns = new ArrayList<>(); + final ArrayList templates = new ArrayList<>(); + tables.add(new DatasourceStructure.Table( + DatasourceStructure.TableType.COLLECTION, + collectionName, + columns, + new ArrayList<>(), + templates + )); + + final Document first = database.getCollection(collectionName).find().limit(1).first(); + if (first == null) { + continue; + } + + String filterFieldName = null; + String filterFieldValue = null; + Map sampleInsertValues = new LinkedHashMap<>(); + + for (Map.Entry entry : first.entrySet()) { + final String name = entry.getKey(); + final Object value = entry.getValue(); + String type; + + if (value instanceof Integer) { + type = "Integer"; + sampleInsertValues.put(name, "1"); + } else if (value instanceof Long) { + type = "Long"; + sampleInsertValues.put(name, "NumberLong(\"1\")"); + } else if (value instanceof Double) { + type = "Double"; + sampleInsertValues.put(name, "1"); + } else if (value instanceof Decimal128) { + type = "BigDecimal"; + sampleInsertValues.put(name, "NumberDecimal(\"1\")"); + } else if (value instanceof String) { + type = "String"; + sampleInsertValues.put(name, "\"new value\""); + if (filterFieldName == null || filterFieldName.compareTo(name) > 0) { + filterFieldName = name; + filterFieldValue = (String) value; + } + } else if (value instanceof ObjectId) { + type = "ObjectId"; + if (!value.equals("_id")) { + sampleInsertValues.put(name, "ObjectId(\"a_valid_object_id_hex\")"); + } + } else if (value instanceof Collection) { + type = "Array"; + sampleInsertValues.put(name, "[1, 2, 3]"); + } else if (value instanceof Date) { + type = "Date"; + sampleInsertValues.put(name, "new Date(\"2019-07-01\")"); + } else { + type = "Object"; + sampleInsertValues.put(name, "{}"); + } + + columns.add(new DatasourceStructure.Column(name, type, null)); + } + + columns.sort(Comparator.naturalOrder()); + + templates.add( + new DatasourceStructure.Template( + "Find", + "{\n" + + " \"find\": \"" + collectionName + "\",\n" + + ( + filterFieldName == null ? "" : + " \"filter\": {\n" + + " \"" + filterFieldName + "\": \"" + filterFieldValue + "\"\n" + + " },\n" + ) + + " \"sort\": {\n" + + " \"_id\": 1\n" + + " },\n" + + " \"limit\": 10\n" + + "}\n" + ) + ); + + templates.add( + new DatasourceStructure.Template( + "Find by ID", + "{\n" + + " \"find\": \"" + collectionName + "\",\n" + + " \"filter\": {\n" + + " \"_id\": ObjectId(\"id_to_query_with\")\n" + + " }\n" + + "}\n" + ) + ); + + sampleInsertValues.entrySet().stream() + .map(entry -> " \"" + entry.getKey() + "\": " + entry.getValue() + ",\n") + .collect(Collectors.joining("")); + templates.add( + new DatasourceStructure.Template( + "Insert", + "{\n" + + " \"insert\": \"" + collectionName + "\",\n" + + " \"documents\": [\n" + + " {\n" + + sampleInsertValues.entrySet().stream() + .map(entry -> " \"" + entry.getKey() + "\": " + entry.getValue() + ",\n") + .sorted() + .collect(Collectors.joining("")) + + " }\n" + + " ]\n" + + "}\n" + ) + ); + + templates.add( + new DatasourceStructure.Template( + "Update", + "{\n" + + " \"update\": \"" + collectionName + "\",\n" + + " \"updates\": [\n" + + " {\n" + + " \"q\": {\n" + + " \"_id\": ObjectId(\"id_of_document_to_update\")\n" + + " },\n" + + " \"u\": { \"$set\": { \"" + filterFieldName + "\": \"new value\" } }\n" + + " }\n" + + " ]\n" + + "}\n" + ) + ); + + templates.add( + new DatasourceStructure.Template( + "Delete", + "{\n" + + " \"delete\": \"" + collectionName + "\",\n" + + " \"deletes\": [\n" + + " {\n" + + " \"q\": {\n" + + " \"_id\": \"id_of_document_to_delete\"\n" + + " },\n" + + " \"limit\": 1\n" + + " }\n" + + " ]\n" + + "}\n" + ) + ); + } + + tables.sort(Comparator.comparing(DatasourceStructure.Table::getName)); + return Mono.just(structure); } } + private static String urlEncode(String text) { + return URLEncoder.encode(text, StandardCharsets.UTF_8); + } + private static Object cleanUp(Object object) { if (object instanceof JSONObject) { JSONObject jsonObject = (JSONObject) object; diff --git a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java index 6d43b410f0..8b013cc6c6 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java @@ -4,6 +4,7 @@ import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.Connection; import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.Endpoint; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -23,6 +24,7 @@ import java.time.LocalDate; import java.util.List; import java.util.Map; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -48,8 +50,8 @@ public class MongoPluginTest { final MongoClient mongoClient = new MongoClient(address, port); if (!mongoClient.getDatabase("test").listCollectionNames().iterator().hasNext()) { - final MongoCollection usersCOllection = mongoClient.getDatabase("test").getCollection("users"); - usersCOllection.insertMany(List.of( + final MongoCollection usersCollection = mongoClient.getDatabase("test").getCollection("users"); + usersCollection.insertMany(List.of( new Document(Map.of( "name", "Cierra Vega", "gender", "F", @@ -213,4 +215,97 @@ public class MongoPluginTest { .verifyComplete(); } + @Test + public void testStructure() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono structureMono = pluginExecutor.datasourceCreate(dsConfig) + .flatMap(connection -> pluginExecutor.getStructure(connection, dsConfig)); + + StepVerifier.create(structureMono) + .assertNext(structure -> { + assertNotNull(structure); + assertEquals(1, structure.getTables().size()); + + final DatasourceStructure.Table possessionsTable = structure.getTables().get(0); + assertEquals("users", possessionsTable.getName()); + assertEquals(DatasourceStructure.TableType.COLLECTION, possessionsTable.getType()); + assertArrayEquals( + new DatasourceStructure.Column[]{ + new DatasourceStructure.Column("_id", "ObjectId", null), + new DatasourceStructure.Column("age", "Integer", null), + new DatasourceStructure.Column("dob", "Date", null), + new DatasourceStructure.Column("gender", "String", null), + new DatasourceStructure.Column("luckyNumber", "Long", null), + new DatasourceStructure.Column("name", "String", null), + new DatasourceStructure.Column("netWorth", "BigDecimal", null), + }, + possessionsTable.getColumns().toArray() + ); + + assertArrayEquals( + new DatasourceStructure.Key[]{}, + possessionsTable.getKeys().toArray() + ); + + assertArrayEquals( + new DatasourceStructure.Template[]{ + new DatasourceStructure.Template("Find", "{\n" + + " \"find\": \"users\",\n" + + " \"filter\": {\n" + + " \"gender\": \"F\"\n" + + " },\n" + + " \"sort\": {\n" + + " \"_id\": 1\n" + + " },\n" + + " \"limit\": 10\n" + + "}\n"), + new DatasourceStructure.Template("Find by ID", "{\n" + + " \"find\": \"users\",\n" + + " \"filter\": {\n" + + " \"_id\": ObjectId(\"id_to_query_with\")\n" + + " }\n" + + "}\n"), + new DatasourceStructure.Template("Insert", "{\n" + + " \"insert\": \"users\",\n" + + " \"documents\": [\n" + + " {\n" + + " \"_id\": ObjectId(\"a_valid_object_id_hex\"),\n" + + " \"age\": 1,\n" + + " \"dob\": new Date(\"2019-07-01\"),\n" + + " \"gender\": \"new value\",\n" + + " \"luckyNumber\": NumberLong(\"1\"),\n" + + " \"name\": \"new value\",\n" + + " \"netWorth\": NumberDecimal(\"1\"),\n" + + " }\n" + + " ]\n" + + "}\n"), + new DatasourceStructure.Template("Update", "{\n" + + " \"update\": \"users\",\n" + + " \"updates\": [\n" + + " {\n" + + " \"q\": {\n" + + " \"_id\": ObjectId(\"id_of_document_to_update\")\n" + + " },\n" + + " \"u\": { \"$set\": { \"gender\": \"new value\" } }\n" + + " }\n" + + " ]\n" + + "}\n"), + new DatasourceStructure.Template("Delete", "{\n" + + " \"delete\": \"users\",\n" + + " \"deletes\": [\n" + + " {\n" + + " \"q\": {\n" + + " \"_id\": \"id_of_document_to_delete\"\n" + + " },\n" + + " \"limit\": 1\n" + + " }\n" + + " ]\n" + + "}\n"), + }, + possessionsTable.getTemplates().toArray() + ); + }) + .verifyComplete(); + } + } diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java index e3754a0b14..8529e28a47 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java @@ -261,8 +261,12 @@ public class MySqlPlugin extends BasePlugin { connection.setReadOnly( configurationConnection != null && READ_ONLY.equals(configurationConnection.getMode())); return Mono.just(connection); - } catch (SQLException e) { - return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error connecting to MySQL: " + e.getMessage(), e)); + } catch (SQLException error) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Error connecting to MySQL: " + error.getMessage(), + error + )); } } diff --git a/contributions/docs/Actions.md b/contributions/docs/Actions.md new file mode 100644 index 0000000000..7ef25b00d4 --- /dev/null +++ b/contributions/docs/Actions.md @@ -0,0 +1,27 @@ +# Action docs + +Steps to contribute: +1. Create a appsmith-docs/actions/.md file. +2. Follow the [asset-upload](UploadingAssets.md) guidelines to upload and use an asset in the docs. + +## Action docs template +Copy paste this template in the file you are updating +``` +--- +description: >- + Action Description +--- + +# ActionName + +## Parameters +| Param | Description | +| :--------- | :----------------- | +| **param1** | param1 description | +| **param2** | param2 description | +| **param3** | param3 description | + +## Image/gif of the action used in a widget + +## Example +``` diff --git a/contributions/docs/CONTRIBUTING.md b/contributions/docs/CONTRIBUTING.md new file mode 100644 index 0000000000..49177021f5 --- /dev/null +++ b/contributions/docs/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# Contributing to Appsmith Documentation + +Thank you for your interest in Appsmith and taking the time out to contribute to our documentation. 🙌 +Feel free to propose changes to this document in a pull request. + +Appsmith uses Gitbook for [documentation](https://docs.appsmith.com). The docs are backed by the [appsmith-docs](https://github.com/appsmithorg/appsmith-docs) repository + +## Suggesting Improvements + +If you feel parts of our documentation can be improved or have incorrect information, you can open a new issue using our [Documentation Template](https://github.com/appsmithorg/appsmith/issues/new?assignees=Nikhil-Nandagopal&labels=Documentation&template=---documentation-improvement.md&title=%5BDocs%5D+) + +## Contributing + +Our [good first issues](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3ADocumentation+label%3A%22Good+First+Issue%22) list is the best place to begin contributing + +### Updating the docs + +Before raising a pull request, ensure you have raised a corresponding issue and discussed it with a maintainer. This gives your pull request the highest chance of getting merged quickly. + +1. Fork the [appsmith-docs](https://github.com/appsmithorg/appsmith-docs) repo and branch out from the default branch. +2. If a new release is being created, contact a maintainer to update the default branch to mirror the new release version. +3. Read our [guidelines](#guidelines) for the section you wish to update +4. Add / Update the relevant files and commit them with a clear commit message +3. Create a pull request in your fork to the default branch in the appsmithorg/appsmith-docs base repository +4. Link the issue of the base repository in your Pull request description. [Guide](https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) + +## Guidelines + +To maintain consistency, we have a set structure for the different types of documentation pages on appsmith. Refer to the following when updating the docs + +- [Documenting Widgets](docs/Widgets.md) +- [Documenting Actions](docs/Actions.md) +- [Documenting DB Integrations](docs/DB%20Integrations.md) +- [Uploading Assets](docs/UploadingAssets.md) diff --git a/contributions/docs/DB Integrations.md b/contributions/docs/DB Integrations.md new file mode 100644 index 0000000000..f15fcc5a57 --- /dev/null +++ b/contributions/docs/DB Integrations.md @@ -0,0 +1,21 @@ +# DB Integration docs +Create a appsmith-docs/db-integrations/.md file. +Follow the [asset-upload](./asset-upload.md) guidelines to upload and use an asset in docs. + +## Action docs template +Copy paste this template +``` +--- +description: >- + Integration Description +--- + +# Integration Name + +## Link to the official documentation + +## Example of Passing Data from widgets + +## Example of Displaying Data in widgets +``` + diff --git a/contributions/docs/UploadingAssets.md b/contributions/docs/UploadingAssets.md new file mode 100644 index 0000000000..d9057b0c93 --- /dev/null +++ b/contributions/docs/UploadingAssets.md @@ -0,0 +1,18 @@ +# Uploading and using assets +Upload images & gifs to the `appsmith-docs/.gitbook/assets/` folder. +Asset files cannot be larger than 25mb. +Refer to the assets using a relative path from the root folder Ex. `.gitbook/assets/asset.jpg` + +## Embedding Images & Gifs +Images are gifs can be embedded into a page using the following syntax +``` +![Click to expand](/.gitbook/assets/asset-name.png) +``` + +## Embedding Videos +Videos must be uploaded to the appsmith youtube channel. Contact nikhil@appsmith.com to have your video uploaded. +Videos must be of very high quality and abide by our [Code of Conduct](/CODE_OF_CONDUCT.md) +Videos can be embedded inside pages using the following syntax +``` +{% embed url="https://www.youtube.com/watch?v=mzqK0QIZRLs&feature=youtu.be" %} +``` diff --git a/contributions/docs/Widgets.md b/contributions/docs/Widgets.md new file mode 100644 index 0000000000..7297f22d74 --- /dev/null +++ b/contributions/docs/Widgets.md @@ -0,0 +1,32 @@ +# Widget docs +1. Create a appsmith-docs/widget-reference/.md file. +2. Follow the [asset-upload](UploadingAssets.md) guidelines to upload and use an asset in the docs. + +## Widget docs template +Copy paste this template +``` +--- +description: >- + Widget Description +--- + +# WidgetName + +## Image/gif of the widget on the canvas with the icon of the widget in the sidebar + +## 2 different usages of the widget with API / Query data + +## Properties +| Property | Description | +| :------------ | :-------------------- | +| **property1** | Property1 description | +| **property2** | Property2 description | +| **property3** | Property3 description | + +| Action | Description | +| :---------- | :------------------ | +| **action1** | action1 description | +| **action2** | action2 description | +| **action3** | action3 description | +``` + From 27c7dbd139e339368e5cee760ee6bb2cdbcc0eb1 Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Thu, 1 Oct 2020 15:49:58 +0530 Subject: [PATCH 3/9] Update CONTRIBUTING.md --- contributions/docs/CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contributions/docs/CONTRIBUTING.md b/contributions/docs/CONTRIBUTING.md index 49177021f5..4e105e0b51 100644 --- a/contributions/docs/CONTRIBUTING.md +++ b/contributions/docs/CONTRIBUTING.md @@ -28,7 +28,7 @@ Before raising a pull request, ensure you have raised a corresponding issue and To maintain consistency, we have a set structure for the different types of documentation pages on appsmith. Refer to the following when updating the docs -- [Documenting Widgets](docs/Widgets.md) -- [Documenting Actions](docs/Actions.md) -- [Documenting DB Integrations](docs/DB%20Integrations.md) -- [Uploading Assets](docs/UploadingAssets.md) +- [Documenting Widgets](Widgets.md) +- [Documenting Actions](Actions.md) +- [Documenting DB Integrations](DB%20Integrations.md) +- [Uploading Assets](UploadingAssets.md) From 902bfa33dc894f8989aebbcf67011b2fadf51643 Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Thu, 1 Oct 2020 16:19:20 +0530 Subject: [PATCH 4/9] Update Actions.md --- contributions/docs/Actions.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/contributions/docs/Actions.md b/contributions/docs/Actions.md index 7ef25b00d4..dad929f8ff 100644 --- a/contributions/docs/Actions.md +++ b/contributions/docs/Actions.md @@ -1,27 +1,29 @@ # Action docs Steps to contribute: -1. Create a appsmith-docs/actions/.md file. +1. Create a appsmith-docs/action-reference/.md file. 2. Follow the [asset-upload](UploadingAssets.md) guidelines to upload and use an asset in the docs. ## Action docs template Copy paste this template in the file you are updating -``` +```bash --- description: >- - Action Description + Description of the action --- -# ActionName +# Action Name -## Parameters -| Param | Description | -| :--------- | :----------------- | -| **param1** | param1 description | -| **param2** | param2 description | -| **param3** | param3 description | +## Signature -## Image/gif of the action used in a widget +## + function signature of the action wrapped in code blocks -## Example +#### Arguments + +| Argument Name | Description | +| :--- | :--- | +| **Argument Name** | Argument Description | + +## Image / gif of the action used ``` From bf989a24dbe477b7e52c35d727b79c7ce4bf9c13 Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Thu, 1 Oct 2020 16:24:02 +0530 Subject: [PATCH 5/9] Update and rename Actions.md to InternalFunctions.md --- contributions/docs/{Actions.md => InternalFunctions.md} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename contributions/docs/{Actions.md => InternalFunctions.md} (86%) diff --git a/contributions/docs/Actions.md b/contributions/docs/InternalFunctions.md similarity index 86% rename from contributions/docs/Actions.md rename to contributions/docs/InternalFunctions.md index dad929f8ff..161ab77160 100644 --- a/contributions/docs/Actions.md +++ b/contributions/docs/InternalFunctions.md @@ -1,15 +1,15 @@ -# Action docs +# Internal Function docs Steps to contribute: 1. Create a appsmith-docs/action-reference/.md file. 2. Follow the [asset-upload](UploadingAssets.md) guidelines to upload and use an asset in the docs. -## Action docs template +## Functions docs template Copy paste this template in the file you are updating ```bash --- description: >- - Description of the action + Description of the function --- # Action Name From 004218d5aa5b710b92d9c0149dedb9bd9fc40f6f Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Thu, 1 Oct 2020 16:25:38 +0530 Subject: [PATCH 6/9] Update InternalFunctions.md --- contributions/docs/InternalFunctions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributions/docs/InternalFunctions.md b/contributions/docs/InternalFunctions.md index 161ab77160..e7a123798e 100644 --- a/contributions/docs/InternalFunctions.md +++ b/contributions/docs/InternalFunctions.md @@ -1,7 +1,7 @@ # Internal Function docs Steps to contribute: -1. Create a appsmith-docs/action-reference/.md file. +1. Create a appsmith-docs/function-reference/.md file. 2. Follow the [asset-upload](UploadingAssets.md) guidelines to upload and use an asset in the docs. ## Functions docs template From b0abc8f2bdd0c0b347eb0647d701a2a5ef1482d8 Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Thu, 1 Oct 2020 16:26:02 +0530 Subject: [PATCH 7/9] Update CONTRIBUTING.md --- contributions/docs/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributions/docs/CONTRIBUTING.md b/contributions/docs/CONTRIBUTING.md index 4e105e0b51..1d5de15335 100644 --- a/contributions/docs/CONTRIBUTING.md +++ b/contributions/docs/CONTRIBUTING.md @@ -29,6 +29,6 @@ Before raising a pull request, ensure you have raised a corresponding issue and To maintain consistency, we have a set structure for the different types of documentation pages on appsmith. Refer to the following when updating the docs - [Documenting Widgets](Widgets.md) -- [Documenting Actions](Actions.md) +- [Documenting Actions](InternalFunctions.md) - [Documenting DB Integrations](DB%20Integrations.md) - [Uploading Assets](UploadingAssets.md) From 2b7bbbba58f978948b4976952d1e48fac5dc560a Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Thu, 1 Oct 2020 16:26:27 +0530 Subject: [PATCH 8/9] Update CONTRIBUTING.md --- contributions/docs/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributions/docs/CONTRIBUTING.md b/contributions/docs/CONTRIBUTING.md index 1d5de15335..25972c502b 100644 --- a/contributions/docs/CONTRIBUTING.md +++ b/contributions/docs/CONTRIBUTING.md @@ -29,6 +29,6 @@ Before raising a pull request, ensure you have raised a corresponding issue and To maintain consistency, we have a set structure for the different types of documentation pages on appsmith. Refer to the following when updating the docs - [Documenting Widgets](Widgets.md) -- [Documenting Actions](InternalFunctions.md) +- [Documenting Functions](InternalFunctions.md) - [Documenting DB Integrations](DB%20Integrations.md) - [Uploading Assets](UploadingAssets.md) From 264b326ca7fb3aab12cda2cea0a2b2a7f8eb17ba Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Thu, 1 Oct 2020 16:28:15 +0530 Subject: [PATCH 9/9] Update InternalFunctions.md --- contributions/docs/InternalFunctions.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/contributions/docs/InternalFunctions.md b/contributions/docs/InternalFunctions.md index e7a123798e..0b27a73343 100644 --- a/contributions/docs/InternalFunctions.md +++ b/contributions/docs/InternalFunctions.md @@ -1,7 +1,7 @@ # Internal Function docs Steps to contribute: -1. Create a appsmith-docs/function-reference/.md file. +1. Create a appsmith-docs/function-reference/.md file. 2. Follow the [asset-upload](UploadingAssets.md) guidelines to upload and use an asset in the docs. ## Functions docs template @@ -12,12 +12,12 @@ description: >- Description of the function --- -# Action Name +# Function Name ## Signature ## - function signature of the action wrapped in code blocks + function signature wrapped in code blocks #### Arguments @@ -25,5 +25,8 @@ description: >- | :--- | :--- | | **Argument Name** | Argument Description | -## Image / gif of the action used +## Image / gif of the function being used ``` + +[Example Doc](https://github.com/appsmithorg/appsmith-docs/blob/v1.2/function-reference/show-modal.md) +