Skip to content

Commit

Permalink
Feature: data query deletion (ToolJet#734)
Browse files Browse the repository at this point in the history
* add feature for data query deletion

* fix after query deletion default query selection is not being done

* move delete option and hide option unless mouse is hovered on query

* show dialog box confirmation before query deletion
  • Loading branch information
akshaysasidrn authored Sep 13, 2021
1 parent 8791f69 commit f637a80
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 60 deletions.
83 changes: 66 additions & 17 deletions frontend/src/Editor/Editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ class Editor extends React.Component {
apps: [],
dataQueriesDefaultText: "You haven't created queries yet.",
showQuerySearchField: false,
isDeletingDataQuery: false,
showHiddenOptionsForDataQueryId: null,
};
}

Expand Down Expand Up @@ -159,16 +161,9 @@ class Editor extends React.Component {
});

// Select first query by default
let selectedQuery = this.state.selectedQuery;
let editingQuery = false;

if (selectedQuery) {
data.data_queries.find((dq) => dq.id === selectedQuery.id);
editingQuery = true;
} else if (data.data_queries.length > 0) {
selectedQuery = data.data_queries[0];
editingQuery = true;
}
let selectedQuery =
data.data_queries.find((dq) => dq.id === this.state.selectedQuery?.id) || data.data_queries[0];
let editingQuery = selectedQuery ? true : false;

this.setState({
selectedQuery,
Expand Down Expand Up @@ -331,14 +326,41 @@ class Editor extends React.Component {
);
};

deleteDataQuery = () => {
this.setState({ showDataQueryDeletionConfirmation: true });
}

cancelDeleteDataQuery = () => {
this.setState({ showDataQueryDeletionConfirmation: false});
}

executeDataQueryDeletion = () => {
this.setState({ showDataQueryDeletionConfirmation: false, isDeletingDataQuery: true });
dataqueryService
.del(this.state.selectedQuery.id)
.then(() => {
toast.success('Query Deleted', { hideProgressBar: true, position: 'bottom-center' });
this.setState({ isDeletingDataQuery: false });
this.dataQueriesChanged();
})
.catch(({ error }) => {
this.setState({ isDeletingDataQuery: false });
toast.error(error, { hideProgressBar: true, position: 'bottom-center' });
});
};

setShowHiddenOptionsForDataQuery = (dataQueryId) => {
this.setState({ showHiddenOptionsForDataQueryId: dataQueryId });
}

renderDataQuery = (dataQuery) => {
const sourceMeta = DataSourceTypes.find((source) => source.kind === dataQuery.kind);

let isSeletedQuery = false;
if (this.state.selectedQuery) {
isSeletedQuery = dataQuery.id === this.state.selectedQuery.id;
}

const isQueryBeingDeleted = this.state.isDeletingDataQuery && isSeletedQuery
const { currentState } = this.state;

const isLoading = currentState.queries[dataQuery.name] ? currentState.queries[dataQuery.name].isLoading : false;
Expand All @@ -349,6 +371,8 @@ class Editor extends React.Component {
key={dataQuery.name}
onClick={() => this.setState({ editingQuery: true, selectedQuery: dataQuery })}
role="button"
onMouseEnter={() => this.setShowHiddenOptionsForDataQuery(dataQuery.id)}
onMouseLeave={() => this.setShowHiddenOptionsForDataQuery(null)}
>
<div className="col">
<img
Expand All @@ -358,8 +382,29 @@ class Editor extends React.Component {
/>
<span className="p-3">{dataQuery.name}</span>
</div>
<div className="col-auto mx-1">
{ isQueryBeingDeleted ? (
<div className="px-2">
<div className="text-center spinner-border spinner-border-sm" role="status"></div>
</div>
) : (
<button
className="btn badge bg-azure-lt"
onClick={this.deleteDataQuery}
style={{ display: this.state.showHiddenOptionsForDataQueryId === dataQuery.id ? 'block' : 'none' }}
>
<div>
<img src="/assets/images/icons/trash.svg" width="12" height="12" className="mx-1" />
</div>
</button>
)}
</div>
<div className="col-auto">
{!(isLoading === true) && (
{isLoading === true ? (
<div className="px-2">
<div className="text-center spinner-border spinner-border-sm" role="status"></div>
</div>
) : (
<button
className="btn badge bg-azure-lt"
onClick={() => {
Expand All @@ -376,11 +421,6 @@ class Editor extends React.Component {
</div>
</button>
)}
{isLoading === true && (
<div className="px-2">
<div className="text-center spinner-border spinner-border-sm" role="status"></div>
</div>
)}
</div>
</div>
);
Expand Down Expand Up @@ -475,6 +515,8 @@ class Editor extends React.Component {
scaleValue,
dataQueriesDefaultText,
showQuerySearchField,
showDataQueryDeletionConfirmation,
isDeletingDataQuery,
apps,
defaultComponentStateComputed
} = this.state;
Expand All @@ -492,6 +534,13 @@ class Editor extends React.Component {
onCancel={() => onQueryCancel(this)}
queryConfirmationData={this.state.queryConfirmationData}
/>
<Confirm
show={showDataQueryDeletionConfirmation}
message={'Do you really want to delete this query?'}
confirmButtonLoading={isDeletingDataQuery}
onConfirm={() => this.executeDataQueryDeletion()}
onCancel={() => this.cancelDeleteDataQuery()}
/>
<DndProvider backend={HTML5Backend}>
<div className="header">
<header className="navbar navbar-expand-md navbar-light d-print-none">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/Editor/QueryManager/QueryManager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ class QueryManager extends React.Component {
/>
<span className="form-check-label">Show notification on success?</span>
</label>

{this.state.options.showSuccessNotification &&
<div>

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/_services/dataquery.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const dataqueryService = {
getAll,
run,
update,
del,
preview
};

Expand Down Expand Up @@ -37,6 +38,11 @@ function update(id, name, options) {
return fetch(`${config.apiUrl}/data_queries/${id}`, requestOptions).then(handleResponse);
}

function del(id) {
const requestOptions = { method: 'DELETE', headers: authHeader() };
return fetch(`${config.apiUrl}/data_queries/${id}`, requestOptions).then(handleResponse);
}

function run(queryId, options) {
const body = {
options: options
Expand Down
45 changes: 37 additions & 8 deletions server/src/controllers/data_queries.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { Controller, Get, Param, Post, Patch, Query, Request, UseGuards, ForbiddenException } from '@nestjs/common';
import {
Controller,
Get,
Param,
Post,
Patch,
Delete,
Query,
Request,
UseGuards,
ForbiddenException,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
import { decamelizeKeys } from 'humps';
import { DataQueriesService } from '../../src/services/data_queries.service';
Expand Down Expand Up @@ -28,7 +39,7 @@ export class DataQueriesController {
if(!ability.can('getQueries', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
}

const queries = await this.dataQueriesService.all(req.user, query.app_id);
const seralizedQueries = [];

Expand Down Expand Up @@ -57,7 +68,7 @@ export class DataQueriesController {
if(!ability.can('createQuery', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
}

const dataSourceId = req.body.data_source_id;

// Make sure that the data source belongs ot the app
Expand All @@ -67,7 +78,7 @@ export class DataQueriesController {
throw new ForbiddenException('you do not have permissions to perform this action');
}
}

const dataQuery = await this.dataQueriesService.create(req.user, name, kind, options, appId, dataSourceId);
return decamelizeKeys(dataQuery);
}
Expand All @@ -84,13 +95,31 @@ export class DataQueriesController {
if(!ability.can('updateQuery', dataQuery.app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
}

const result = await this.dataQueriesService.update(req.user, dataQueryId, name, options);
return decamelizeKeys(result);
}

@UseGuards(JwtAuthGuard)
@Delete(':id')
async delete(@Request() req, @Param() params) {
const dataQueryId = params.id;

const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
const ability = await this.appsAbilityFactory.appsActions(req.user, {});

if (!ability.can('deleteQuery', dataQuery.app)) {
throw new ForbiddenException(
'you do not have permissions to perform this action',
);
}

const result = await this.dataQueriesService.delete(params.id);
return decamelizeKeys(result);
}

@UseGuards(QueryAuthGuard)
@Post(':id/run')
@Post(':id/run')
async runQuery(@Request() req, @Param() params) {
const dataQueryId = params.id;
const { options } = req.body;
Expand Down Expand Up @@ -132,7 +161,7 @@ export class DataQueriesController {
}

@UseGuards(JwtAuthGuard)
@Post('/preview')
@Post('/preview')
async previewQuery(@Request() req, @Param() params) {
const { options, query } = req.body;
const dataQueryEntity = {
Expand Down Expand Up @@ -170,7 +199,7 @@ export class DataQueriesController {
}
}
}

return result;
}

Expand Down
71 changes: 49 additions & 22 deletions server/src/modules/casl/abilities/apps-ability.factory.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,66 @@
import { User } from 'src/entities/user.entity';
import { InferSubjects, AbilityBuilder, Ability, AbilityClass, ExtractSubjectType } from '@casl/ability';
import {
InferSubjects,
AbilityBuilder,
Ability,
AbilityClass,
ExtractSubjectType,
} from '@casl/ability';
import { Injectable } from '@nestjs/common';
import { App } from 'src/entities/app.entity';
import { AppVersion } from 'src/entities/app_version.entity';

type Actions = 'updateParams' | 'fetchUsers' | 'createUsers' | 'fetchVersions' | 'createVersions'
| 'updateVersions' | 'viewApp' | 'runQuery' | 'updateQuery' | 'getQueries'| 'previewQuery' | 'createQuery'
| 'getDataSources' | 'updateDataSource' | 'createDataSource' | 'authorizeOauthForSource' | 'deleteApp';
type Actions =
| 'updateParams'
| 'fetchUsers'
| 'createUsers'
| 'fetchVersions'
| 'createVersions'
| 'updateVersions'
| 'viewApp'
| 'runQuery'
| 'updateQuery'
| 'deleteQuery'
| 'getQueries'
| 'previewQuery'
| 'createQuery'
| 'getDataSources'
| 'updateDataSource'
| 'createDataSource'
| 'authorizeOauthForSource'
| 'deleteApp';

type Subjects = InferSubjects<typeof AppVersion| typeof User | typeof App> | 'all';
type Subjects =
| InferSubjects<typeof AppVersion | typeof User | typeof App>
| 'all';

export type AppsAbility = Ability<[Actions, Subjects]>;

@Injectable()
export class AppsAbilityFactory {

async appsActions(user: User, params: any) {
const { can, cannot, build } = new AbilityBuilder<
const { can, cannot, build } = new AbilityBuilder<
Ability<[Actions, Subjects]>
>(Ability as AbilityClass<AppsAbility>);

// Only admins can update app params such as name, friendly url & visibility
if(user.isAdmin) {
can('updateParams', App, { organizationId: user.organizationId } );
can('createUsers', App, { organizationId: user.organizationId } );
can('deleteApp', App, { organizationId: user.organizationId } );
if (user.isAdmin) {
can('updateParams', App, { organizationId: user.organizationId });
can('createUsers', App, { organizationId: user.organizationId });
can('deleteApp', App, { organizationId: user.organizationId });
}

// Only developers and admins can create new versions
if(user.isAdmin || user.isDeveloper) {
can('createVersions', App, { organizationId: user.organizationId } );
can('updateVersions', App, { organizationId: user.organizationId } );

can('updateQuery', App, { organizationId: user.organizationId } );
can('createQuery', App, { organizationId: user.organizationId } );

can('updateDataSource', App, { organizationId: user.organizationId } );
can('createDataSource', App, { organizationId: user.organizationId } );
if (user.isAdmin || user.isDeveloper) {
can('createVersions', App, { organizationId: user.organizationId });
can('updateVersions', App, { organizationId: user.organizationId });

can('updateQuery', App, { organizationId: user.organizationId });
can('createQuery', App, { organizationId: user.organizationId });
can('deleteQuery', App, { organizationId: user.organizationId });

can('updateDataSource', App, { organizationId: user.organizationId });
can('createDataSource', App, { organizationId: user.organizationId });
}

// All organization users can view the app users
Expand All @@ -54,10 +78,13 @@ export class AppsAbilityFactory {

// policies for datasources
can('getDataSources', App, { organizationId: user.organizationId });
can('authorizeOauthForSource', App, { organizationId: user.organizationId });
can('authorizeOauthForSource', App, {
organizationId: user.organizationId,
});

return build({
detectSubjectType: item => item.constructor as ExtractSubjectType<Subjects>
detectSubjectType: (item) =>
item.constructor as ExtractSubjectType<Subjects>,
});
}
}
Loading

0 comments on commit f637a80

Please sign in to comment.