Skip to content

Commit

Permalink
Merge pull request galaxyproject#17219 from guerler/grids_history
Browse files Browse the repository at this point in the history
Vueifiy History Grids
  • Loading branch information
mvdbeek authored Jan 15, 2024
2 parents 390a8fe + 262cd25 commit ff173ed
Show file tree
Hide file tree
Showing 27 changed files with 1,132 additions and 328 deletions.
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 @@ -6352,6 +6356,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 @@ -14017,6 +14076,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

0 comments on commit ff173ed

Please sign in to comment.