Skip to content

Commit 9556173

Browse files
committed
Add "Unassigned Queries" view
1 parent 4ed0ad3 commit 9556173

File tree

7 files changed

+145
-44
lines changed

7 files changed

+145
-44
lines changed

client/app/pages/queries-list/QueriesList.jsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ const sidebarMenu = [
3535
title: "All Queries",
3636
icon: () => <Sidebar.MenuIcon icon="fa fa-code" />,
3737
},
38+
{
39+
key: "unassigned",
40+
href: "queries/unassigned",
41+
title: "Unassigned Queries",
42+
icon: () => <Sidebar.MenuIcon icon="fa fa-code" />,
43+
},
3844
{
3945
key: "my",
4046
href: "queries/my",
@@ -194,6 +200,7 @@ const QueriesListPage = itemsList(
194200
my: Query.myQueries.bind(Query),
195201
favorites: Query.favorites.bind(Query),
196202
archive: Query.archive.bind(Query),
203+
unassigned: Query.unassigned.bind(Query),
197204
}[currentPage];
198205
},
199206
getItemProcessor() {
@@ -219,6 +226,14 @@ routes.register(
219226
render: (pageProps) => <QueriesListPage {...pageProps} currentPage="favorites" />,
220227
})
221228
);
229+
routes.register(
230+
"Queries.Unassigned",
231+
routeWithUserSession({
232+
path: "/queries/unassigned",
233+
title: "Unassigned Queries",
234+
render: (pageProps) => <QueriesListPage {...pageProps} currentPage="unassigned" />,
235+
})
236+
);
222237
routes.register(
223238
"Queries.Archived",
224239
routeWithUserSession({

client/app/services/query.js

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const logger = debug("redash:services:query");
3333
function collectParams(parts) {
3434
let parameters = [];
3535

36-
parts.forEach(part => {
36+
parts.forEach((part) => {
3737
if (part[0] === "name" || part[0] === "&") {
3838
parameters.push(part[1].split(".")[0]);
3939
} else if (part[0] === "#") {
@@ -68,12 +68,7 @@ export class Query {
6868

6969
scheduleInLocalTime() {
7070
const parts = this.schedule.split(":");
71-
return moment
72-
.utc()
73-
.hour(parts[0])
74-
.minute(parts[1])
75-
.local()
76-
.format("HH:mm");
71+
return moment.utc().hour(parts[0]).minute(parts[1]).local().format("HH:mm");
7772
}
7873

7974
hasResult() {
@@ -158,11 +153,11 @@ export class Query {
158153

159154
let params = {};
160155
if (this.getParameters().isRequired()) {
161-
this.getParametersDefs().forEach(param => {
156+
this.getParametersDefs().forEach((param) => {
162157
extend(params, param.toUrlParams());
163158
});
164159
}
165-
Object.keys(params).forEach(key => params[key] == null && delete params[key]);
160+
Object.keys(params).forEach((key) => params[key] == null && delete params[key]);
166161
params = map(params, (value, name) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join("&");
167162

168163
if (params !== "") {
@@ -220,7 +215,7 @@ class Parameters {
220215
}
221216

222217
parseQuery() {
223-
const fallback = () => map(this.query.options.parameters, i => i.name);
218+
const fallback = () => map(this.query.options.parameters, (i) => i.name);
224219

225220
let parameters = [];
226221
if (this.query.query !== undefined) {
@@ -242,26 +237,26 @@ class Parameters {
242237
updateParameters(update) {
243238
if (this.query.query === this.cachedQueryText) {
244239
const parameters = this.query.options.parameters;
245-
const hasUnprocessedParameters = find(parameters, p => !(p instanceof Parameter));
240+
const hasUnprocessedParameters = find(parameters, (p) => !(p instanceof Parameter));
246241
if (hasUnprocessedParameters) {
247-
this.query.options.parameters = map(parameters, p =>
242+
this.query.options.parameters = map(parameters, (p) =>
248243
p instanceof Parameter ? p : createParameter(p, this.query.id)
249244
);
250245
}
251246
return;
252247
}
253248

254249
this.cachedQueryText = this.query.query;
255-
const parameterNames = update ? this.parseQuery() : map(this.query.options.parameters, p => p.name);
250+
const parameterNames = update ? this.parseQuery() : map(this.query.options.parameters, (p) => p.name);
256251

257252
this.query.options.parameters = this.query.options.parameters || [];
258253

259254
const parametersMap = {};
260-
this.query.options.parameters.forEach(param => {
255+
this.query.options.parameters.forEach((param) => {
261256
parametersMap[param.name] = param;
262257
});
263258

264-
parameterNames.forEach(param => {
259+
parameterNames.forEach((param) => {
265260
if (!has(parametersMap, param)) {
266261
this.query.options.parameters.push(
267262
createParameter({
@@ -275,15 +270,15 @@ class Parameters {
275270
}
276271
});
277272

278-
const parameterExists = p => includes(parameterNames, p.name);
273+
const parameterExists = (p) => includes(parameterNames, p.name);
279274
const parameters = this.query.options.parameters;
280275
this.query.options.parameters = parameters
281276
.filter(parameterExists)
282-
.map(p => (p instanceof Parameter ? p : createParameter(p, this.query.id)));
277+
.map((p) => (p instanceof Parameter ? p : createParameter(p, this.query.id)));
283278
}
284279

285280
initFromQueryString(query) {
286-
this.get().forEach(param => {
281+
this.get().forEach((param) => {
287282
param.fromUrlParams(query);
288283
});
289284
}
@@ -294,16 +289,16 @@ class Parameters {
294289
}
295290

296291
add(parameterDef) {
297-
this.query.options.parameters = this.query.options.parameters.filter(p => p.name !== parameterDef.name);
292+
this.query.options.parameters = this.query.options.parameters.filter((p) => p.name !== parameterDef.name);
298293
const param = createParameter(parameterDef);
299294
this.query.options.parameters.push(param);
300295
return param;
301296
}
302297

303298
getMissing() {
304299
return map(
305-
filter(this.get(), p => p.isEmpty),
306-
i => i.title
300+
filter(this.get(), (p) => p.isEmpty),
301+
(i) => i.title
307302
);
308303
}
309304

@@ -314,28 +309,28 @@ class Parameters {
314309
getExecutionValues(extra = {}) {
315310
const params = this.get();
316311
return zipObject(
317-
map(params, i => i.name),
318-
map(params, i => i.getExecutionValue(extra))
312+
map(params, (i) => i.name),
313+
map(params, (i) => i.getExecutionValue(extra))
319314
);
320315
}
321316

322317
hasPendingValues() {
323-
return some(this.get(), p => p.hasPendingValue);
318+
return some(this.get(), (p) => p.hasPendingValue);
324319
}
325320

326321
applyPendingValues() {
327-
each(this.get(), p => p.applyPendingValue());
322+
each(this.get(), (p) => p.applyPendingValue());
328323
}
329324

330325
toUrlParams() {
331326
if (this.get().length === 0) {
332327
return "";
333328
}
334329

335-
const params = Object.assign(...this.get().map(p => p.toUrlParams()));
336-
Object.keys(params).forEach(key => params[key] == null && delete params[key]);
330+
const params = Object.assign(...this.get().map((p) => p.toUrlParams()));
331+
Object.keys(params).forEach((key) => params[key] == null && delete params[key]);
337332
return Object.keys(params)
338-
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
333+
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
339334
.join("&");
340335
}
341336
}
@@ -374,26 +369,27 @@ export class QueryResultError {
374369
}
375370
}
376371

377-
const getQuery = query => new Query(query);
378-
const saveOrCreateUrl = data => (data.id ? `api/queries/${data.id}` : "api/queries");
379-
const mapResults = data => ({ ...data, results: map(data.results, getQuery) });
372+
const getQuery = (query) => new Query(query);
373+
const saveOrCreateUrl = (data) => (data.id ? `api/queries/${data.id}` : "api/queries");
374+
const mapResults = (data) => ({ ...data, results: map(data.results, getQuery) });
380375

381376
const QueryService = {
382-
query: params => axios.get("api/queries", { params }).then(mapResults),
383-
get: data => axios.get(`api/queries/${data.id}`, data).then(getQuery),
384-
save: data => axios.post(saveOrCreateUrl(data), data).then(getQuery),
385-
delete: data => axios.delete(`api/queries/${data.id}`),
386-
recent: params => axios.get(`api/queries/recent`, { params }).then(data => map(data, getQuery)),
387-
archive: params => axios.get(`api/queries/archive`, { params }).then(mapResults),
388-
myQueries: params => axios.get("api/queries/my", { params }).then(mapResults),
377+
query: (params) => axios.get("api/queries", { params }).then(mapResults),
378+
get: (data) => axios.get(`api/queries/${data.id}`, data).then(getQuery),
379+
save: (data) => axios.post(saveOrCreateUrl(data), data).then(getQuery),
380+
delete: (data) => axios.delete(`api/queries/${data.id}`),
381+
recent: (params) => axios.get(`api/queries/recent`, { params }).then((data) => map(data, getQuery)),
382+
unassigned: (params) => axios.get(`api/queries/unassigned`, { params }).then(mapResults),
383+
archive: (params) => axios.get(`api/queries/archive`, { params }).then(mapResults),
384+
myQueries: (params) => axios.get("api/queries/my", { params }).then(mapResults),
389385
fork: ({ id }) => axios.post(`api/queries/${id}/fork`, { id }).then(getQuery),
390-
resultById: data => axios.get(`api/queries/${data.id}/results.json`),
391-
asDropdown: data => axios.get(`api/queries/${data.id}/dropdown`),
386+
resultById: (data) => axios.get(`api/queries/${data.id}/results.json`),
387+
asDropdown: (data) => axios.get(`api/queries/${data.id}/dropdown`),
392388
associatedDropdown: ({ queryId, dropdownQueryId }) =>
393389
axios.get(`api/queries/${queryId}/dropdowns/${dropdownQueryId}`),
394-
favorites: params => axios.get("api/queries/favorites", { params }).then(mapResults),
395-
favorite: data => axios.post(`api/queries/${data.id}/favorite`),
396-
unfavorite: data => axios.delete(`api/queries/${data.id}/favorite`),
390+
favorites: (params) => axios.get("api/queries/favorites", { params }).then(mapResults),
391+
favorite: (data) => axios.post(`api/queries/${data.id}/favorite`),
392+
unfavorite: (data) => axios.delete(`api/queries/${data.id}/favorite`),
397393
};
398394

399395
QueryService.newQuery = function newQuery() {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
describe("Query list sort", () => {
2+
beforeEach(() => {
3+
cy.login();
4+
});
5+
6+
describe("Sorting table does not crash page ", () => {
7+
it("sorts", () => {
8+
cy.visit("/queries");
9+
cy.contains("Name").click();
10+
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
11+
cy.getByTestId("ErrorMessage").should("not.exist");
12+
});
13+
});
14+
15+
describe("Show only unassigned queries ", () => {
16+
it("sorts", () => {
17+
cy.visit("/queries/unassigned");
18+
cy.contains("Name").click();
19+
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
20+
cy.getByTestId("ErrorMessage").should("not.exist");
21+
});
22+
});
23+
});

redash/handlers/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
QueryResource,
6969
QuerySearchResource,
7070
QueryTagsResource,
71+
QueryUnassignedResource,
7172
)
7273
from redash.handlers.query_results import (
7374
JobResource,
@@ -202,6 +203,7 @@ def json_representation(data, code, headers=None):
202203

203204
api.add_org_resource(QuerySearchResource, "/api/queries/search", endpoint="queries_search")
204205
api.add_org_resource(QueryRecentResource, "/api/queries/recent", endpoint="recent_queries")
206+
api.add_org_resource(QueryUnassignedResource, "/api/queries/unassigned", endpoint="unassigned_queries")
205207
api.add_org_resource(QueryArchiveResource, "/api/queries/archive", endpoint="queries_archive")
206208
api.add_org_resource(QueryListResource, "/api/queries", endpoint="queries")
207209
api.add_org_resource(MyQueriesResource, "/api/queries/my", endpoint="my_queries")

redash/handlers/queries.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,28 @@ def post(self):
245245
return QuerySerializer(query, with_visualizations=True).serialize()
246246

247247

248+
class QueryUnassignedResource(BaseQueryListResource):
249+
def get_queries(self, search_term):
250+
if search_term:
251+
return models.Query.search(
252+
search_term,
253+
self.current_user.group_ids,
254+
self.current_user.id,
255+
include_drafts=False,
256+
include_archived=False,
257+
unassigned_only=True,
258+
multi_byte_search=current_org.get_setting("multi_byte_search_enabled"),
259+
)
260+
else:
261+
return models.Query.all_queries(
262+
self.current_user.group_ids,
263+
self.current_user.id,
264+
include_drafts=False,
265+
include_archived=False,
266+
unassigned_only=True,
267+
)
268+
269+
248270
class QueryArchiveResource(BaseQueryListResource):
249271
def get_queries(self, search_term):
250272
if search_term:

redash/models/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ def create(cls, **kwargs):
519519
return query
520520

521521
@classmethod
522-
def all_queries(cls, group_ids, user_id=None, include_drafts=False, include_archived=False):
522+
def all_queries(cls, group_ids, user_id=None, include_drafts=False, include_archived=False, unassigned_only=False):
523523
query_ids = (
524524
db.session.query(distinct(cls.id))
525525
.join(DataSourceGroup, Query.data_source_id == DataSourceGroup.data_source_id)
@@ -538,6 +538,17 @@ def all_queries(cls, group_ids, user_id=None, include_drafts=False, include_arch
538538
.options(contains_eager(Query.user), contains_eager(Query.latest_query_data))
539539
)
540540

541+
if unassigned_only:
542+
excluded_query_ids = (
543+
db.session.query(distinct(cls.id))
544+
.join(Visualization)
545+
.join(Widget)
546+
.join(Dashboard)
547+
.filter(cls.id.in_(query_ids))
548+
.filter(Dashboard.is_archived.is_(False))
549+
)
550+
queries = queries.filter(~cls.id.in_(excluded_query_ids))
551+
541552
if not include_drafts:
542553
queries = queries.filter(or_(Query.is_draft.is_(False), Query.user_id == user_id))
543554
return queries
@@ -653,13 +664,15 @@ def search(
653664
include_drafts=False,
654665
limit=None,
655666
include_archived=False,
667+
unassigned_only=False,
656668
multi_byte_search=False,
657669
):
658670
all_queries = cls.all_queries(
659671
group_ids,
660672
user_id=user_id,
661673
include_drafts=include_drafts,
662674
include_archived=include_archived,
675+
unassigned_only=unassigned_only,
663676
)
664677

665678
if multi_byte_search:

tests/handlers/test_queries.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,36 @@ def test_prevents_association_with_non_existing_dropdown_queries(self):
326326
self.assertEqual(rv.status_code, 400)
327327

328328

329+
class TestQueryUnassignedResourceGet(BaseTestCase):
330+
def test_returns_queries(self):
331+
q1 = self.factory.create_query()
332+
self.factory.create_query(is_archived=True)
333+
334+
rv = self.make_request("get", "/api/queries/unassigned")
335+
336+
assert len(rv.json["results"]) == 1
337+
assert set([result["id"] for result in rv.json["results"]]) == {q1.id}
338+
339+
def test_search_term(self):
340+
q1 = self.factory.create_query(name="Sales")
341+
q2 = self.factory.create_query(name="Q1 sales")
342+
self.factory.create_query(name="Q2 sales", is_archived=True)
343+
344+
rv = self.make_request("get", "/api/queries/unassigned?q=sales")
345+
assert len(rv.json["results"]) == 2
346+
assert set([result["id"] for result in rv.json["results"]]) == {q1.id, q2.id}
347+
348+
def test_returns_queries_only_unassigned(self):
349+
q1 = self.factory.create_query()
350+
self.factory.create_widget()
351+
352+
rv = self.make_request("get", "/api/queries")
353+
assert len(rv.json["results"]) == 2
354+
rv = self.make_request("get", "/api/queries/unassigned")
355+
assert len(rv.json["results"]) == 1
356+
assert set([result["id"] for result in rv.json["results"]]) == {q1.id}
357+
358+
329359
class TestQueryArchiveResourceGet(BaseTestCase):
330360
def test_returns_queries(self):
331361
q1 = self.factory.create_query(is_archived=True)

0 commit comments

Comments
 (0)