Skip to content

Commit

Permalink
Merge pull request galaxyproject#16642 from ahmedhamidawan/published_…
Browse files Browse the repository at this point in the history
…history_pagination

Add scroll pagination and username filter to `HistoryPublishedList`
  • Loading branch information
martenson authored Sep 8, 2023
2 parents 33aab28 + 8b41093 commit 174257d
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 59 deletions.
240 changes: 182 additions & 58 deletions client/src/components/History/HistoryPublishedList.vue
Original file line number Diff line number Diff line change
@@ -1,44 +1,63 @@
<script setup>
import { useInfiniteScroll } from "@vueuse/core";
import Heading from "components/Common/Heading";
import DebouncedInput from "components/DebouncedInput";
import LoadingSpan from "components/LoadingSpan";
import StatelessTags from "components/TagsMultiselect/StatelessTags";
import ScrollToTopButton from "components/ToolsList/ScrollToTopButton";
import UtcDate from "components/UtcDate";
import Filtering, { contains, expandNameTag } from "utils/filtering";
import { computed, ref, watch } from "vue";
import { useAnimationFrameScroll } from "composables/sensors/animationFrameScroll";
import Filtering, { contains, equals, expandNameTag } from "utils/filtering";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { getPublishedHistories, updateTags } from "./services";
const LIMIT = 50;
const validFilters = {
name: contains("name"),
annotation: contains("annotation"),
tag: contains("tags", "tag", expandNameTag),
user: contains("username"),
user_eq: equals("username"),
};
const filters = new Filtering(validFilters, false);
const limit = ref(50);
const offset = ref(0);
const items = ref([]);
const perPage = ref(50);
const message = ref(null);
const loading = ref(true);
const props = defineProps({
fUsername: {
type: String,
required: false,
default: null,
},
});
const offset = ref({ "-update_time-true": 0 });
const allHistories = ref([]);
const results = ref([]);
const error = ref(null);
const loading = ref(false);
const sortDesc = ref(true);
const currentPage = ref(1);
const filterText = ref("");
const showAdvanced = ref(false);
const sortBy = ref("update_time");
const scrollableDiv = ref(null);
const { scrollTop } = useAnimationFrameScroll(scrollableDiv);
const loadedItemIds = ref(new Set());
const noItems = computed(() => !loading.value && items.value.length === 0 && !filterText.value);
const noResults = computed(() => !loading.value && items.value.length === 0 && filterText.value);
const noItems = computed(() => !loading.value && allHistories.value.length === 0 && !filterText.value);
const noResults = computed(() => !loading.value && results.value.length === 0 && filterText.value);
const allLoaded = computed(() => {
return noResults.value === false && offset.value[getOffsetKey(sortBy.value, sortDesc.value)] === null;
});
const fields = [
{ key: "name", sortable: true },
const fields = computed(() => [
{ key: "name", sortable: !loading.value },
{ key: "annotation", sortable: false },
{ label: "Owner", key: "username", sortable: false },
{ label: "Tags", key: "tags", sortable: false },
{ label: "Last Updated", key: "update_time", sortable: true },
];
{ label: "Last Updated", key: "update_time", sortable: !loading.value },
]);
const localFilter = computed({
get() {
Expand All @@ -57,57 +76,138 @@ const updateFilter = (newVal) => {
filterText.value = newVal.trim();
};
const onTagClick = (tag) => {
updateFilter(filters.setFilterValue(filterText.value, "tag", tag));
const setFilter = (filter, tag) => {
updateFilter(filters.setFilterValue(filterText.value, filter, tag));
};
const onTagsUpdate = (newTags, row) => {
row.item.tags = newTags;
updateTags(row.item.id, "History", row.item.tags);
};
const onToggle = () => {
showAdvanced.value = !showAdvanced.value;
};
const load = async () => {
const onSearch = () => {
onToggle();
updateFilter(filters.getFilterText(filterSettings.value));
};
const scrollToTop = () => {
scrollableDiv.value.scrollTo({ top: 0, behavior: "smooth" });
};
const sortAndFilterHistories = () => {
allHistories.value = allHistories.value.sort((a, b) => {
const aVal = String(a[sortBy.value]).trim();
const bVal = String(b[sortBy.value]).trim();
if (!sortDesc.value) {
return aVal - bVal;
} else {
return bVal - aVal;
}
});
if (filterText.value) {
const currFilters = filters.getFiltersForText(filterText.value);
results.value = allHistories.value.filter((history) => {
if (!filters.testFilters(currFilters, history)) {
return false;
}
return true;
});
} else {
// no filter
results.value = allHistories.value;
}
};
function getOffsetKey(sort_by, sort_desc) {
return `${filters.getQueryString(filterText.value)}-${sort_by}-${sort_desc}`;
}
async function load() {
if (loading.value) {
return;
}
loading.value = true;
if (allLoaded.value) {
sortAndFilterHistories();
loading.value = false;
error.value = null;
return;
}
getPublishedHistories(
// Define offsetKey in offset ref if it doesn't exist
const currOffsetKey = getOffsetKey(sortBy.value, sortDesc.value);
let currentOffset = offset.value[currOffsetKey];
if (currentOffset === undefined) {
offset.value[currOffsetKey] = 0;
currentOffset = 0;
}
await getPublishedHistories(
{
limit: limit.value,
offset: offset.value,
limit: LIMIT,
offset: currentOffset,
sortBy: sortBy.value,
sortDesc: sortDesc.value,
filterText: filterText.value,
},
filters
)
.then((data) => {
items.value = data;
// add all new incoming histories to allHistories
const newData = data.filter((item) => !loadedItemIds.value.has(item.id));
allHistories.value = [...allHistories.value, ...newData];
newData.forEach((item) => loadedItemIds.value.add(item.id));
// sort and filter allHistories containing new histories
sortAndFilterHistories();
// ------ UPDATE OFFSET ------
if (results.value.length > currentOffset - 1 && data.length >= 50) {
const addToOffset = filterText.value ? 1 : 0;
offset.value[currOffsetKey] = results.value.length === 0 ? 0 : results.value.length + addToOffset;
} else {
// All items loaded for current filter/offsetKey
offset.value[getOffsetKey("update_time", true)] = null;
offset.value[getOffsetKey("update_time", false)] = null;
offset.value[getOffsetKey("name", true)] = null;
offset.value[getOffsetKey("name", false)] = null;
}
// ---------------------------
error.value = null;
})
.catch((error) => {
message.value = error;
error.value = error;
})
.finally(() => {
loading.value = false;
});
};
const onTagsUpdate = (newTags, row) => {
row.item.tags = newTags;
updateTags(row.item.id, "History", row.item.tags);
};
}
const onToggle = () => {
showAdvanced.value = !showAdvanced.value;
};
const onSearch = () => {
onToggle();
updateFilter(filters.getFilterText(filterSettings.value));
};
onMounted(async () => {
if (props.fUsername) {
filterText.value = filters.getFilterText({ "user_eq:": props.fUsername });
}
await load();
useInfiniteScroll(scrollableDiv.value, () => load());
});
load();
onUnmounted(() => {
// Remove the infinite scrolling behavior
useInfiniteScroll(scrollableDiv.value, () => {});
});
watch([filterText, sortBy, sortDesc], () => {
load();
watch([filterText, sortBy, sortDesc], async () => {
await load();
});
</script>

<template>
<section id="published-histories" class="d-flex flex-column">
<section id="published-histories" class="d-flex flex-column position-relative overflow-hidden">
<Heading h1>Published Histories</Heading>

<b-alert v-if="noItems" variant="info" show>No published histories found.</b-alert>
Expand Down Expand Up @@ -164,6 +264,12 @@ watch([filterText, sortBy, sortDesc], () => {
v-model="filterSettings['tag:']"
size="sm"
placeholder="any community tag" />
<small class="mt-1">Filter by owner:</small>
<b-form-input
id="published-histories-advanced-filter-username"
v-model="filterSettings['user:']"
size="sm"
placeholder="any username" />
<div class="mt-3">
<b-button
id="published-histories-advanced-filter-submit"
Expand All @@ -182,51 +288,69 @@ watch([filterText, sortBy, sortDesc], () => {
</div>
</div>

<b-alert v-if="noResults" variant="info" show>
No matching entries found for: <span class="font-weight-bold">{{ filterText }}</span>
<b-alert v-if="noResults || error" :variant="error ? 'danger' : 'info'" show>
<div>
No matching entries found for: <span class="font-weight-bold">{{ filterText }}</span>
</div>
<div v-if="error">
<i>{{ error }}</i>
</div>
</b-alert>

<b-alert v-if="loading" variant="info" show>
<b-alert v-if="results.length === 0 && loading" variant="info" show>
<LoadingSpan message="Loading published histories" />
</b-alert>
</div>

<div ref="scrollableDiv" class="overflow-auto">
<b-table
v-if="items.length"
v-if="results.length"
id="published-histories-table"
no-sort-reset
no-local-sorting
striped
:fields="fields"
:items="items"
:per-page="perPage"
:current-page="currentPage"
:items="results"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc">
<template v-slot:cell(name)="row">
<router-link :to="`/published/history?id=${row.item.id}`">
{{ row.item.name }}
</router-link>
</template>
<template v-slot:cell(username)="row">
<a
href="#"
class="published-histories-username-link"
@click="setFilter('user_eq', row.item.username)"
>{{ row.item.username }}</a
>
</template>
<template v-slot:cell(tags)="row">
<StatelessTags
clickable
:value="row.item.tags"
:disabled="true"
@input="(tags) => onTagsUpdate(tags, row)"
@tag-click="onTagClick" />
@tag-click="(tag) => setFilter('tag', tag)" />
</template>

<template v-slot:cell(update_time)="data">
<UtcDate :date="data.value" mode="elapsed" />
<UtcDate v-if="data.value" :date="data.value" mode="elapsed" />
<span v-else> - </span>
</template>
</b-table>

<b-pagination
v-if="items.length > perPage"
v-model="currentPage"
:per-page="perPage"
:total-rows="items.length"
align="center" />
<div v-if="allLoaded" class="list-end my-2">- End of search results -</div>
<b-overlay :show="loading" opacity="0.5" />
<ScrollToTopButton :offset="scrollTop" @click="scrollToTop" />
</div>
</section>
</template>

<style lang="scss">
@import "theme/blue.scss";
.list-end {
width: 100%;
text-align: center;
color: $text-light;
}
</style>
6 changes: 5 additions & 1 deletion client/src/entry/analysis/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,11 @@ export function getRouter(Galaxy) {
{
path: "histories/list_published",
component: HistoryPublishedList,
props: true,
props: (route) => {
return {
...route.query,
};
},
},
{
path: "histories/archived",
Expand Down
16 changes: 16 additions & 0 deletions lib/galaxy/managers/histories.py
Original file line number Diff line number Diff line change
Expand Up @@ -884,3 +884,19 @@ def _add_parsers(self):
"update_time": {"op": ("le", "ge", "gt", "lt"), "val": self.parse_date},
}
)
self.fn_filter_parsers.update(
{
"username": {
"op": {
"eq": self.username_eq,
"contains": self.username_contains,
},
},
}
)

def username_eq(self, item, val: str) -> bool:
return val.lower() == str(item.user.username).lower()

def username_contains(self, item, val: str) -> bool:
return val.lower() in str(item.user.username).lower()
Loading

0 comments on commit 174257d

Please sign in to comment.