Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vueifiy History Grids #17219

Merged
merged 50 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
1dda750
Add histories grid to router
guerler Dec 9, 2023
69b616a
Add query endpoint to history api
guerler Dec 9, 2023
2dbbbb2
Add service layer for history query endpoint
guerler Dec 9, 2023
edd60e6
Add draft of history query to history manager
guerler Dec 9, 2023
7faae38
Add attributes to the history serializer?
guerler Dec 9, 2023
0260d45
Sketch out history grid operations
guerler Dec 9, 2023
ee65fbf
Add advanced filter options to history grid
guerler Dec 12, 2023
2f15d3a
Add delete, undelete and purge operations to history grid, fix naming
guerler Dec 12, 2023
2c0ef7a
Remove copy operation modal from history grid for now
guerler Dec 12, 2023
61f44af
Add history stats grid column
guerler Dec 12, 2023
441c0a0
Add tags handler, adjust dataset state box
guerler Dec 13, 2023
ffcce81
Fix naming, prep username filter
guerler Dec 13, 2023
beccfd3
Add initial draft of history shared grid config
guerler Dec 14, 2023
ad06e70
Remove false condition from visualizations grid
guerler Dec 19, 2023
3ba70f1
Lint and remove unused imports
guerler Dec 20, 2023
7acb3a7
Add history schema
guerler Dec 20, 2023
d645a7b
Switch user manager from session query to stmt
guerler Dec 23, 2023
b33f4d9
Adjust query allow for sorting by username
guerler Dec 23, 2023
591d8e5
Fix alignment of history states
guerler Dec 23, 2023
c4873ea
Adjust selenium tests
guerler Dec 23, 2023
104f223
Adjust histories advanced filter selenium tests
guerler Dec 27, 2023
150ac44
Adjust selenium history grid delete and permanently delete tests
guerler Dec 27, 2023
beae41b
Adjust histories grid sorting test, adjust alert message for purging …
guerler Dec 27, 2023
80291dd
Adjust grid list operation conditions
guerler Dec 27, 2023
664af7e
Adjust grid tag cell test for histories
guerler Dec 28, 2023
e8b76ab
Move grid cell selector helper to navigates galaxy
guerler Dec 28, 2023
49937da
Restore advanced filter history list by tag test
guerler Dec 28, 2023
e34ae1a
Consolidate history purge api schema
guerler Dec 28, 2023
8405fb2
Resolve typescript errors in sharing component
guerler Dec 28, 2023
9d9490e
Avoid sharing status when item has been deleted or purged
guerler Dec 28, 2023
6441830
Fix visualizations manager test condition
guerler Dec 28, 2023
57c544e
Sort history list by update time by default
guerler Dec 29, 2023
bbf5d25
Adjust sort handling in grid list
guerler Dec 29, 2023
ee3af6e
Avoid wrapping grid operation icon around label
guerler Dec 29, 2023
5079e63
Remove legacy history grid wrappers
guerler Dec 30, 2023
53b218b
Add filter option for purged histories
guerler Dec 30, 2023
698aeb2
Update client/src/components/Grid/configs/historiesShared.ts
guerler Jan 8, 2024
d664147
Fix root model
guerler Jan 11, 2024
dd68d44
Use pydantic config dict
guerler Jan 11, 2024
cb7903a
Fix import from history schema
guerler Jan 11, 2024
418629b
Remove redundant HistoryIDPathParam from history api
guerler Jan 11, 2024
b4daf7d
Remove async keyword from history query endpoint
guerler Jan 11, 2024
bd4a78a
Update client schema
guerler Jan 11, 2024
20d0e18
Remove unused DecodedDatabaseIdField from history manager
guerler Jan 11, 2024
cb62dfe
Fix linting
guerler Jan 11, 2024
97a7b22
Use root instead of __root__ in history services
guerler Jan 11, 2024
a4c04e3
Fix failing tags selenium test by delaying evaluation
guerler Jan 12, 2024
9b529a0
Explicitly wait for history grid, move column number to named variable
guerler Jan 12, 2024
8582ec5
Update lib/galaxy/model/__init__.py
guerler Jan 12, 2024
262cd25
Fix linting
guerler Jan 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion client/src/api/histories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import { fetcher } from "@/api/schema";

export const historiesFetcher = fetcher.path("/api/histories").method("get").create();
export const archivedHistoriesFetcher = fetcher.path("/api/histories/archived").method("get").create();
export const deleteHistory = fetcher.path("/api/histories/{history_id}").method("delete").create();
export const undeleteHistory = fetcher.path("/api/histories/deleted/{history_id}/undelete").method("post").create();
export const purgeHistory = fetcher.path("/api/histories/{history_id}").method("delete").create();
export const historiesQuery = fetcher.path("/api/histories/query").method("get").create();
129 changes: 129 additions & 0 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,10 @@ export interface paths {
/** Return all histories that are published. */
get: operations["published_api_histories_published_get"];
};
"/api/histories/query": {
/** Returns histories available to the current user. */
get: operations["query_api_histories_query_get"];
};
"/api/histories/shared_with_me": {
/** Return all histories that are shared with the current user. */
get: operations["shared_with_me_api_histories_shared_with_me_get"];
Expand Down Expand Up @@ -6347,6 +6351,61 @@ export interface components {
user_id?: string | null;
[key: string]: unknown | undefined;
};
/** HistoryQueryResult */
HistoryQueryResult: {
/**
* Annotation
* @description The annotation of this History.
*/
annotation?: string | null;
/**
* Create Time
* @description The time and date this item was created.
*/
create_time: string | null;
/**
* Deleted
* @description Whether this History has been deleted.
*/
deleted: boolean;
/**
* ID
* @description Encoded ID of the History.
* @example 0123456789ABCDEF
*/
id: string;
/**
* Importable
* @description Whether this History can be imported.
*/
importable: boolean;
/**
* Name
* @description The name of the History.
*/
name: string;
/**
* Published
* @description Whether this History has been published.
*/
published: boolean;
/**
* Tags
* @description A list of tags to add to this item.
*/
tags: components["schemas"]["TagCollection"] | null;
/**
* Update Time
* @description The last time and date this item was updated.
*/
update_time: string | null;
[key: string]: unknown | undefined;
};
/**
* HistoryQueryResultList
* @default []
*/
HistoryQueryResultList: components["schemas"]["HistoryQueryResult"][];
/**
* HistorySummary
* @description History summary information.
Expand Down Expand Up @@ -14012,6 +14071,76 @@ export interface operations {
};
};
};
query_api_histories_query_get: {
/** Returns histories available to the current user. */
parameters?: {
/** @description The maximum number of items to return. */
/** @description Starts at the beginning skip the first ( offset - 1 ) items and begin returning at the Nth item */
/** @description Sort index by this specified attribute */
/** @description Sort in descending order? */
/**
* @description A mix of free text and GitHub-style tags used to filter the index operation.
*
* ## Query Structure
*
* GitHub-style filter tags (not be confused with Galaxy tags) are tags of the form
* `<tag_name>:<text_no_spaces>` or `<tag_name>:'<text with potential spaces>'`. The tag name
* *generally* (but not exclusively) corresponds to the name of an attribute on the model
* being indexed (i.e. a column in the database).
*
* If the tag is quoted, the attribute will be filtered exactly. If the tag is unquoted,
* generally a partial match will be used to filter the query (i.e. in terms of the implementation
* this means the database operation `ILIKE` will typically be used).
*
* Once the tagged filters are extracted from the search query, the remaining text is just
* used to search various documented attributes of the object.
*
* ## GitHub-style Tags Available
*
* `name`
* : The history's name.
*
* `annotation`
* : The history's annotation. (The tag `a` can be used a short hand alias for this tag to filter on this attribute.)
*
* `tag`
* : The history's tags. (The tag `t` can be used a short hand alias for this tag to filter on this attribute.)
*
* ## Free Text
*
* Free text search terms will be searched against the following attributes of the
* Historys: `title`, `description`, `slug`, `tag`.
*/
query?: {
limit?: number | null;
offset?: number | null;
show_own?: boolean;
show_published?: boolean;
show_shared?: boolean;
sort_by?: "create_time" | "name" | "update_time" | "username";
sort_desc?: boolean;
search?: string | null;
};
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
header?: {
"run-as"?: string | null;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["HistoryQueryResultList"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
shared_with_me_api_histories_shared_with_me_get: {
/** Return all histories that are shared with the current user. */
parameters?: {
Expand Down
82 changes: 82 additions & 0 deletions client/src/components/Grid/GridElements/GridDatasets.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script setup lang="ts">
import axios from "axios";
import { onMounted, type Ref, ref } from "vue";

import { withPrefix } from "@/utils/redirect";
import { rethrowSimple } from "@/utils/simple-error";

interface Props {
historyId?: string;
}
const props = defineProps<Props>();

interface HistoryStats {
nice_size: string;
contents_active: {
deleted?: number;
hidden?: number;
active?: number;
};
contents_states: {
error?: number;
ok?: number;
new?: number;
running?: number;
queued?: number;
};
}
const historyStats: Ref<HistoryStats | null> = ref(null);

async function getCounts() {
if (props.historyId) {
try {
const { data } = await axios.get(
withPrefix(`/api/histories/${props.historyId}?keys=nice_size,contents_active,contents_states`)
);
historyStats.value = data;
} catch (e) {
rethrowSimple(e);
}
}
}

onMounted(() => {
getCounts();
});
</script>

<template>
<span v-if="historyStats" class="grid-datasets">
<span v-if="historyStats.nice_size" class="mr-2">
{{ historyStats.nice_size }}
</span>
<span
v-for="(stateCount, state) of historyStats.contents_states"
:key="state"
:class="`stats state-color-${state}`"
:title="`${state} datasets`">
{{ stateCount }}
</span>
<span v-if="historyStats.contents_active.deleted" class="stats state-color-deleted" title="Deleted datasets">
{{ historyStats.contents_active.deleted }}
</span>
<span v-if="historyStats.contents_active.hidden" class="stats state-color-hidden" title="Hidden datasets">
{{ historyStats.contents_active.hidden }}
</span>
</span>
</template>

<style lang="scss" scoped>
@import "~bootstrap/scss/bootstrap.scss";
.grid-datasets {
@extend .d-flex;
@extend .text-nowrap;
.stats {
@extend .rounded;
@extend .px-1;
@extend .mr-1;
border-width: 1px;
border-style: solid;
}
}
</style>
5 changes: 3 additions & 2 deletions client/src/components/Grid/GridElements/GridOperations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,16 @@ function hasCondition(conditionHandler: (rowData: RowData, config: GalaxyConfigu
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
class="ui-link font-weight-bold">
class="ui-link font-weight-bold text-nowrap">
<FontAwesomeIcon icon="caret-down" class="fa-lg" />
<span class="font-weight-bold">{{ title }}</span>
</button>
<div class="dropdown-menu" aria-labelledby="dataset-dropdown">
<span v-for="(operation, operationIndex) in operations" :key="operationIndex">
<button
v-if="operation && operation.condition && hasCondition(operation.condition)"
v-if="operation && (!operation.condition || hasCondition(operation.condition))"
class="dropdown-item"
:data-description="`grid operation ${operation.title.toLowerCase()}`"
@click.prevent="emit('execute', operation)">
<icon :icon="operation.icon" />
<span v-localize>{{ operation.title }}</span>
Expand Down
21 changes: 0 additions & 21 deletions client/src/components/Grid/GridHistory.vue

This file was deleted.

10 changes: 5 additions & 5 deletions client/src/components/Grid/GridList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,15 @@ describe("GridList", () => {
expect(wrapper.find("[data-description='grid cell 0-1'] > button").text()).toBe("link-1");
expect(wrapper.find("[data-description='grid cell 1-1'] > button").text()).toBe("link-2");
const firstHeader = wrapper.find("[data-description='grid header 0']");
expect(firstHeader.find("a").text()).toBe("id");
await firstHeader.find("[data-description='grid sort asc']").trigger("click");
expect(firstHeader.find("button").text()).toBe("id");
await firstHeader.find("[data-description='grid sort desc']").trigger("click");
expect(testGrid.getData).toHaveBeenCalledTimes(2);
expect(testGrid.getData.mock.calls[1]).toEqual([0, 25, "", "id", false]);
expect(firstHeader.find("[data-description='grid sort asc']").exists()).toBeFalsy();
expect(firstHeader.find("[data-description='grid sort desc']").exists()).toBeTruthy();
expect(firstHeader.find("[data-description='grid sort desc']").exists()).toBeFalsy();
expect(firstHeader.find("[data-description='grid sort asc']").exists()).toBeTruthy();
const secondHeader = wrapper.find("[data-description='grid header 1']");
expect(secondHeader.find("[data-description='grid sort asc']").exists()).toBeFalsy();
expect(secondHeader.find("[data-description='grid sort desc']").exists()).toBeFalsy();
expect(secondHeader.find("[data-description='grid sort asc']").exists()).toBeFalsy();
});

it("header rendering", async () => {
Expand Down
25 changes: 13 additions & 12 deletions client/src/components/Grid/GridList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { faCaretDown, faCaretUp, faShieldAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { useDebounceFn, useEventBus } from "@vueuse/core";
import { BAlert, BButton, BLink, BPagination } from "bootstrap-vue";
import { BAlert, BButton, BPagination } from "bootstrap-vue";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { useRouter } from "vue-router/composables";

import { FieldHandler, GridConfig, Operation, RowData } from "./configs/types";

import GridBoolean from "./GridElements/GridBoolean.vue";
import GridDatasets from "./GridElements/GridDatasets.vue";
import GridLink from "./GridElements/GridLink.vue";
import GridOperations from "./GridElements/GridOperations.vue";
import GridText from "./GridElements/GridText.vue";
Expand Down Expand Up @@ -150,6 +151,7 @@ function onSearch(query: string) {
function onSort(sortKey: string) {
if (sortBy.value !== sortKey) {
sortBy.value = sortKey;
sortDesc.value = false;
} else {
sortDesc.value = !sortDesc.value;
}
Expand Down Expand Up @@ -246,13 +248,17 @@ watch(operationMessage, () => {
class="text-nowrap px-2"
:data-description="`grid header ${fieldIndex}`">
<span v-if="gridConfig.sortKeys.includes(fieldEntry.key)">
<BLink @click="onSort(fieldEntry.key)">
<BButton
variant="link"
class="text-nowrap font-weight-bold"
:data-description="`grid sort key ${fieldEntry.key}`"
@click="onSort(fieldEntry.key)">
<span>{{ fieldEntry.title || fieldEntry.key }}</span>
<span v-if="sortBy === fieldEntry.key">
<FontAwesomeIcon v-if="sortDesc" icon="caret-up" data-description="grid sort asc" />
<FontAwesomeIcon v-else icon="caret-down" data-description="grid sort desc" />
<FontAwesomeIcon v-if="sortDesc" icon="caret-down" data-description="grid sort desc" />
<FontAwesomeIcon v-else icon="caret-up" data-description="grid sort asc" />
</span>
</BLink>
</BButton>
</span>
<span v-else>{{ fieldEntry.title || fieldEntry.key }}</span>
</th>
Expand All @@ -273,6 +279,7 @@ watch(operationMessage, () => {
:title="rowData[fieldEntry.key]"
@execute="onOperation($event, rowData)" />
<GridBoolean v-else-if="fieldEntry.type == 'boolean'" :value="rowData[fieldEntry.key]" />
<GridDatasets v-else-if="fieldEntry.type == 'datasets'" :historyId="rowData[fieldEntry.key]" />
<GridText v-else-if="fieldEntry.type == 'text'" :text="rowData[fieldEntry.key]" />
<GridLink
v-else-if="fieldEntry.type == 'link'"
Expand All @@ -298,13 +305,7 @@ watch(operationMessage, () => {
</table>
<div class="flex-grow-1 h-100" />
<div v-if="isAvailable" class="grid-footer d-flex justify-content-center pt-3">
<BPagination
v-model="currentPage"
:total-rows="totalRows"
:per-page="limit"
class="m-0"
size="sm"
aria-controls="grid-table" />
<BPagination v-model="currentPage" :total-rows="totalRows" :per-page="limit" class="m-0" size="sm" />
</div>
</div>
</template>
Expand Down
Loading
Loading