mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #21766 from itisAliRH/btable-to-gtable-toolshed
This commit is contained in:
@@ -61,12 +61,24 @@ interface Props {
|
||||
*/
|
||||
fields?: TableField[];
|
||||
|
||||
/**
|
||||
* Filter string to filter items
|
||||
* @default ""
|
||||
*/
|
||||
filter?: string;
|
||||
|
||||
/**
|
||||
* Whether to show hover effect on rows
|
||||
* @default true
|
||||
*/
|
||||
hover?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to hide the table header
|
||||
* @default false
|
||||
*/
|
||||
hideHeader?: boolean;
|
||||
|
||||
/**
|
||||
* Table data items
|
||||
* @default []
|
||||
@@ -103,6 +115,12 @@ interface Props {
|
||||
*/
|
||||
overlayLoading?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use local filtering (client-side) or rely on external filtering (server-side)
|
||||
* @default true
|
||||
*/
|
||||
localFiltering?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use local sorting (client-side) or rely on external sorting (server-side)
|
||||
* @default true
|
||||
@@ -167,12 +185,15 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
containerClass: "",
|
||||
emptyState: () => ({ message: "No data available" }),
|
||||
fields: () => [],
|
||||
filter: "",
|
||||
hover: true,
|
||||
hideHeader: false,
|
||||
items: () => [],
|
||||
loading: false,
|
||||
loadingMessage: "Loading...",
|
||||
loadMoreLoading: false,
|
||||
loadMoreMessage: "Loading more...",
|
||||
localFiltering: true,
|
||||
localSorting: true,
|
||||
overlayLoading: false,
|
||||
selectable: false,
|
||||
@@ -189,6 +210,12 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
* Events emitted by the GTable component
|
||||
*/
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* Emitted when items are filtered
|
||||
* @event filtered
|
||||
*/
|
||||
(e: "filtered", filteredItems: T[]): void;
|
||||
|
||||
/**
|
||||
* Emitted when sort changes
|
||||
* @event sort-changed
|
||||
@@ -216,15 +243,33 @@ const emit = defineEmits<{
|
||||
|
||||
const sortBy = ref<string>(props.sortBy || "update_time");
|
||||
const sortDesc = ref<boolean>(props.sortDesc || true);
|
||||
const expandedRows = ref<Set<number>>(new Set());
|
||||
|
||||
const localItems = computed(() => {
|
||||
const items = props.items || [];
|
||||
let items = props.items || [];
|
||||
|
||||
// If local sorting is disabled, return items as-is
|
||||
if (!props.localSorting) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Apply local filtering if enabled and filter string is provided
|
||||
if (props.localFiltering && props.filter && props.filter.trim() !== "") {
|
||||
const filterLower = props.filter.toLowerCase().trim();
|
||||
items = items.filter((item) => {
|
||||
// Search through all field values
|
||||
return Object.values(item).some((value) => {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
return String(value).toLowerCase().includes(filterLower);
|
||||
});
|
||||
});
|
||||
|
||||
// Emit filtered event with the filtered items
|
||||
emit("filtered", items);
|
||||
}
|
||||
|
||||
// If no sort field is set, return items as-is
|
||||
if (!sortBy.value) {
|
||||
return items;
|
||||
@@ -341,6 +386,26 @@ function onRowSelect(item: T, index: number) {
|
||||
emit("row-select", { item, index, selected: !isSelected });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if row details are expanded
|
||||
*/
|
||||
function isRowExpanded(index: number) {
|
||||
return expandedRows.value.has(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle row details expansion
|
||||
*/
|
||||
function toggleRowDetails(index: number) {
|
||||
if (isRowExpanded(index)) {
|
||||
expandedRows.value.delete(index);
|
||||
} else {
|
||||
expandedRows.value.add(index);
|
||||
}
|
||||
// Trigger reactivity
|
||||
expandedRows.value = new Set(expandedRows.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cell value with optional formatter
|
||||
*/
|
||||
@@ -415,7 +480,7 @@ const getCellId = (tableId: string, fieldKey: string, index: number) => `g-table
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :id="`g-table-container-${props.id}`" class="w-100" :class="containerClass">
|
||||
<div :id="`g-table-container-${props.id}`" :class="containerClass">
|
||||
<!-- Table wrapper -->
|
||||
<BOverlay :show="overlayLoading" rounded="sm" class="position-relative w-100">
|
||||
<div :id="`g-table-wrapper-${props.id}`" class="position-relative w-100">
|
||||
@@ -428,7 +493,7 @@ const getCellId = (tableId: string, fieldKey: string, index: number) => `g-table
|
||||
{ 'table-bordered': bordered },
|
||||
tableClass,
|
||||
]">
|
||||
<thead>
|
||||
<thead v-if="!props.hideHeader">
|
||||
<tr>
|
||||
<th v-if="selectable" class="g-table-select-column">
|
||||
<slot name="head-select">
|
||||
@@ -479,87 +544,106 @@ const getCellId = (tableId: string, fieldKey: string, index: number) => `g-table
|
||||
</thead>
|
||||
|
||||
<tbody v-if="localItems.length > 0">
|
||||
<tr
|
||||
v-for="(item, index) in localItems"
|
||||
:id="getRowId(props.id, index)"
|
||||
:key="index"
|
||||
:class="{
|
||||
'g-table-row-clickable': clickableRows,
|
||||
'g-table-row-selected': isRowSelected(index),
|
||||
}"
|
||||
@click="onRowClick(item, index, $event)">
|
||||
<!-- Selection checkbox column -->
|
||||
<td v-if="selectable" class="g-table-select-column">
|
||||
<BFormCheckbox
|
||||
:id="`${getRowId(props.id, index)}-select`"
|
||||
v-b-tooltip.hover.noninteractive
|
||||
:checked="isRowSelected(index)"
|
||||
title="Select for bulk actions"
|
||||
@click.stop
|
||||
@change="onRowSelect(item, index)" />
|
||||
</td>
|
||||
<template v-for="(item, index) in localItems">
|
||||
<template>
|
||||
<tr
|
||||
:id="getRowId(props.id, index)"
|
||||
:key="`tr` + index"
|
||||
:class="{
|
||||
'g-table-row-clickable': clickableRows,
|
||||
'g-table-row-selected': isRowSelected(index),
|
||||
}"
|
||||
@click="onRowClick(item, index, $event)">
|
||||
<!-- Selection checkbox column -->
|
||||
<td v-if="selectable" class="g-table-select-column">
|
||||
<BFormCheckbox
|
||||
:id="`${getRowId(props.id, index)}-select`"
|
||||
v-b-tooltip.hover.noninteractive
|
||||
:checked="isRowSelected(index)"
|
||||
title="Select for bulk actions"
|
||||
@click.stop
|
||||
@change="onRowSelect(item, index)" />
|
||||
</td>
|
||||
|
||||
<!-- Data columns -->
|
||||
<td
|
||||
v-for="(field, fieldIndex) in props.fields"
|
||||
:id="getCellId(props.id, field.key, index)"
|
||||
:key="field.key"
|
||||
:class="[
|
||||
field.cellClass,
|
||||
field.class,
|
||||
getAlignmentClass(field.align),
|
||||
{ 'hide-on-small': field.hideOnSmall },
|
||||
]">
|
||||
<template v-if="fieldIndex === 0 && getStatusIcon(item, index)">
|
||||
<FontAwesomeIcon
|
||||
v-if="getStatusIcon(item, index)"
|
||||
v-b-tooltip.hover.noninteractive
|
||||
v-bind="getIconProps(item, index)"
|
||||
fixed-width />
|
||||
</template>
|
||||
|
||||
<slot :name="`cell(${field.key})`" :value="item[field.key]" :item="item" :index="index">
|
||||
<span>{{ getCellValue(item, field) }}</span>
|
||||
</slot>
|
||||
</td>
|
||||
|
||||
<!-- Actions column -->
|
||||
<td v-if="props.actions" class="g-table-actions-column">
|
||||
<slot name="actions" :item="item" :index="index">
|
||||
<BDropdown
|
||||
v-b-tooltip.hover.noninteractive
|
||||
no-caret
|
||||
right
|
||||
title="More actions"
|
||||
variant="link"
|
||||
size="lg"
|
||||
toggle-class="text-decoration-none p-0"
|
||||
@click.stop>
|
||||
<template v-slot:button-content>
|
||||
<FontAwesomeIcon :icon="faEllipsisV" fixed-width />
|
||||
<!-- Data columns -->
|
||||
<td
|
||||
v-for="(field, fieldIndex) in props.fields"
|
||||
:id="getCellId(props.id, field.key, index)"
|
||||
:key="field.key"
|
||||
:class="[
|
||||
field.cellClass,
|
||||
field.class,
|
||||
getAlignmentClass(field.align),
|
||||
{ 'hide-on-small': field.hideOnSmall },
|
||||
]">
|
||||
<template v-if="fieldIndex === 0 && getStatusIcon(item, index)">
|
||||
<FontAwesomeIcon
|
||||
v-if="getStatusIcon(item, index)"
|
||||
v-b-tooltip.hover.noninteractive
|
||||
v-bind="getIconProps(item, index)"
|
||||
fixed-width />
|
||||
</template>
|
||||
|
||||
<template v-for="ac in props.actions">
|
||||
<BDropdownItem
|
||||
v-if="ac.visible ?? true"
|
||||
:id="ac.id"
|
||||
:key="ac.id"
|
||||
:disabled="ac.disabled"
|
||||
:size="ac.size || 'sm'"
|
||||
:variant="ac.variant || 'link'"
|
||||
:to="ac.to"
|
||||
:title="ac.title"
|
||||
:href="ac.href"
|
||||
:target="ac.externalLink ? '_blank' : undefined"
|
||||
@click.stop="ac.handler && ac.handler(item, index)">
|
||||
<FontAwesomeIcon v-if="ac.icon" :icon="ac.icon" fixed-width />
|
||||
{{ ac.label }}
|
||||
</BDropdownItem>
|
||||
</template>
|
||||
</BDropdown>
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
<slot
|
||||
:name="`cell(${field.key})`"
|
||||
:value="item[field.key]"
|
||||
:item="item"
|
||||
:index="index"
|
||||
:toggle-details="() => toggleRowDetails(index)">
|
||||
<span>{{ getCellValue(item, field) }}</span>
|
||||
</slot>
|
||||
</td>
|
||||
|
||||
<!-- Actions column -->
|
||||
<td v-if="props.actions" class="g-table-actions-column">
|
||||
<slot name="actions" :item="item" :index="index">
|
||||
<BDropdown
|
||||
v-b-tooltip.hover.noninteractive
|
||||
no-caret
|
||||
right
|
||||
title="More actions"
|
||||
variant="link"
|
||||
size="lg"
|
||||
toggle-class="text-decoration-none p-0"
|
||||
@click.stop>
|
||||
<template v-slot:button-content>
|
||||
<FontAwesomeIcon :icon="faEllipsisV" fixed-width />
|
||||
</template>
|
||||
|
||||
<template v-for="ac in props.actions">
|
||||
<BDropdownItem
|
||||
v-if="ac.visible ?? true"
|
||||
:id="ac.id"
|
||||
:key="ac.id"
|
||||
:disabled="ac.disabled"
|
||||
:size="ac.size || 'sm'"
|
||||
:variant="ac.variant || 'link'"
|
||||
:to="ac.to"
|
||||
:title="ac.title"
|
||||
:href="ac.href"
|
||||
:target="ac.externalLink ? '_blank' : undefined"
|
||||
@click.stop="ac.handler && ac.handler(item, index)">
|
||||
<FontAwesomeIcon v-if="ac.icon" :icon="ac.icon" fixed-width />
|
||||
{{ ac.label }}
|
||||
</BDropdownItem>
|
||||
</template>
|
||||
</BDropdown>
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Row details expansion -->
|
||||
<tr v-if="isRowExpanded(index)" :key="`details-${index}`" class="g-table-details-row">
|
||||
<td :colspan="props.fields.length + (selectable ? 1 : 0) + (props.actions ? 1 : 0)">
|
||||
<slot
|
||||
name="row-details"
|
||||
:item="item"
|
||||
:index="index"
|
||||
:toggle-details="() => toggleRowDetails(index)" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -616,6 +700,10 @@ const getCellId = (tableId: string, fieldKey: string, index: number) => `g-table
|
||||
&.g-table-row-selected {
|
||||
background-color: $brand-light;
|
||||
}
|
||||
|
||||
&.g-table-details-row {
|
||||
background-color: lighten($brand-light, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mount } from "@vue/test-utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import Index from "./Index.vue";
|
||||
import GLink from "@/components/BaseComponents/GLink.vue";
|
||||
|
||||
vi.mock("app");
|
||||
vi.mock("onload/loadConfig", () => ({
|
||||
@@ -42,20 +43,22 @@ describe("InstalledList", () => {
|
||||
},
|
||||
stubs: {
|
||||
RepositoryDetails: true,
|
||||
GLink,
|
||||
LoadingSpan: true,
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
expect(wrapper.find(".loading-message").text()).toBe("Loading installed repositories...");
|
||||
expect(wrapper.find("loadingspan-stub").attributes("message")).toBe("Loading installed repositories");
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find(".installed-message").text()).toBe("2 repositories installed on this instance.");
|
||||
const names = wrapper.findAll(".name");
|
||||
expect(names.length).toBe(2);
|
||||
expect(names.at(0).text()).toBe("name_0");
|
||||
expect(names.at(1).text()).toBe("name_1");
|
||||
const links = wrapper.findAll("a");
|
||||
const links = wrapper.findAllComponents(GLink);
|
||||
expect(links.length).toBe(3);
|
||||
const badge = links.at(1).find(".badge");
|
||||
expect(badge.text()).toBe("Newer version available!");
|
||||
expect(wrapper.find('th[role="columnheader"][aria-colindex="3"] > div').text()).toBe("Tool Shed");
|
||||
expect(wrapper.vm.fields.some((field) => field.key === "tool_shed")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,44 +4,50 @@
|
||||
<div v-else>
|
||||
<LoadingSpan v-if="loading" message="Loading installed repositories" />
|
||||
<div v-else>
|
||||
<b-alert :variant="messageVariant" :show="showMessage">{{ message }}</b-alert>
|
||||
<BAlert :variant="messageVariant" :show="showMessage">{{ message }}</BAlert>
|
||||
|
||||
<div class="m-1">
|
||||
<span class="installed-message text-muted">
|
||||
{{ repositories.length }} repositories installed on this instance.
|
||||
</span>
|
||||
<b-link class="font-weight-bold" @click="toggleMonitor">
|
||||
|
||||
<GLink class="font-weight-bold" @click="toggleMonitor">
|
||||
<span v-if="showMonitor">
|
||||
<span class="fa fa-angle-double-up" />
|
||||
<FontAwesomeIcon :icon="faAngleDoubleUp" />
|
||||
<span>Hide installation progress.</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="fa fa-angle-double-down" />
|
||||
<FontAwesomeIcon :icon="faAngleDoubleDown" />
|
||||
<span>Show installation progress.</span>
|
||||
</span>
|
||||
</b-link>
|
||||
</GLink>
|
||||
</div>
|
||||
|
||||
<Monitor v-if="showMonitor" @onQuery="onQuery" />
|
||||
<b-table
|
||||
|
||||
<GTable
|
||||
id="repository-table"
|
||||
striped
|
||||
:fields="fields"
|
||||
:sort-by="sortBy"
|
||||
:items="repositories"
|
||||
:filter="filter"
|
||||
@filtered="filtered">
|
||||
<template v-slot:cell(name)="row">
|
||||
<b-link href="#" role="button" class="font-weight-bold" @click="row.toggleDetails">
|
||||
<div v-if="!isLatest(row.item)">
|
||||
<b-badge variant="danger" class="mb-2"> Newer version available! </b-badge>
|
||||
<template v-slot:cell(name)="{ item, toggleDetails }">
|
||||
<GLink class="font-weight-bold" @click.prevent="toggleDetails">
|
||||
<div v-if="!isLatest(item)">
|
||||
<BBadge variant="danger" class="mb-2"> Newer version available! </BBadge>
|
||||
</div>
|
||||
<div class="name">{{ row.item.name }}</div>
|
||||
</b-link>
|
||||
<div>{{ row.item.description }}</div>
|
||||
|
||||
<div class="name">{{ item.name }}</div>
|
||||
</GLink>
|
||||
|
||||
<div>{{ item.description }}</div>
|
||||
</template>
|
||||
<template v-slot:row-details="row">
|
||||
<RepositoryDetails :repo="row.item" />
|
||||
|
||||
<template v-slot:row-details="{ item }">
|
||||
<RepositoryDetails :repo="item" />
|
||||
</template>
|
||||
</b-table>
|
||||
</GTable>
|
||||
|
||||
<div v-if="showNotFound">
|
||||
No matching entries found for: <span class="font-weight-bold">{{ filter }}</span
|
||||
>.
|
||||
@@ -51,9 +57,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import Vue from "vue";
|
||||
import { faAngleDoubleDown, faAngleDoubleUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BAlert, BBadge } from "bootstrap-vue";
|
||||
|
||||
import { getAppRoot } from "@/onload/loadConfig";
|
||||
|
||||
@@ -61,12 +69,17 @@ import { Services } from "../services";
|
||||
|
||||
import RepositoryDetails from "./Details.vue";
|
||||
import Monitor from "./Monitor.vue";
|
||||
import GLink from "@/components/BaseComponents/GLink.vue";
|
||||
import GTable from "@/components/Common/GTable.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BAlert,
|
||||
BBadge,
|
||||
FontAwesomeIcon,
|
||||
GLink,
|
||||
GTable,
|
||||
LoadingSpan,
|
||||
Monitor,
|
||||
RepositoryDetails,
|
||||
@@ -79,6 +92,8 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
faAngleDoubleDown,
|
||||
faAngleDoubleUp,
|
||||
error: null,
|
||||
loading: true,
|
||||
message: null,
|
||||
@@ -86,7 +101,6 @@ export default {
|
||||
nRepositories: 0,
|
||||
repositories: [],
|
||||
showMonitor: false,
|
||||
sortBy: "name",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -110,13 +124,12 @@ export default {
|
||||
const fields = [
|
||||
{
|
||||
key: "name",
|
||||
label: "Name",
|
||||
sortable: true,
|
||||
sortByFormatted: (value, key, item) => {
|
||||
return `${this.isLatest(item)}_${value}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "owner",
|
||||
label: "Owner",
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getLocalVue } from "@tests/vitest/helpers";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import flushPromises from "flush-promises";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import Monitor from "./Monitor.vue";
|
||||
@@ -31,19 +31,18 @@ vi.mock("../services", () => ({
|
||||
|
||||
describe("Monitor", () => {
|
||||
it("test monitor", async () => {
|
||||
const localVue = getLocalVue();
|
||||
const wrapper = mount(Monitor, { localVue });
|
||||
await localVue.nextTick();
|
||||
const wrapper = mount(Monitor);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
const headers = wrapper.findAll("th");
|
||||
expect(headers.length).toBe(3);
|
||||
expect(headers.at(0).text()).toBe("Name");
|
||||
expect(headers.at(1).text()).toBe("Status");
|
||||
expect(headers.length).toBe(0);
|
||||
|
||||
const cells = wrapper.findAll("td");
|
||||
expect(cells.length).toBe(6);
|
||||
expect(cells.at(0).text()).toBe("name_0 (owner_0)");
|
||||
expect(cells.at(0).text()).toContain("name_0 (owner_0)");
|
||||
expect(cells.at(1).text()).toContain("status_0_0");
|
||||
expect(cells.at(3).text()).toBe("name_1 (owner_1)");
|
||||
expect(cells.at(3).text()).toContain("name_1 (owner_1)");
|
||||
expect(cells.at(4).text()).toContain("status_1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,45 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-alert v-if="error" variant="danger" show>
|
||||
<BAlert v-if="error" variant="danger" show>
|
||||
{{ error }}
|
||||
</b-alert>
|
||||
<b-card v-if="showItems" no-body class="my-2">
|
||||
</BAlert>
|
||||
|
||||
<GCard v-if="showItems" class="my-2">
|
||||
<h2 class="m-3 h-text">Currently installing...</h2>
|
||||
<b-table
|
||||
class="mx-3 mb-0"
|
||||
sticky-header
|
||||
thead-class="installation-monitor-header"
|
||||
:items="items"
|
||||
:fields="fields">
|
||||
|
||||
<GTable :items="items" :fields="fields" hide-header class="m-2">
|
||||
<template v-slot:cell(name)="row">
|
||||
<b-link @click="onQuery(row.item.name)"> {{ row.item.name }} ({{ row.item.owner }}) </b-link>
|
||||
<GLink tooltip title="Show all repositories with this name" @click="onQuery(row.item.name)">
|
||||
{{ row.item.name }} ({{ row.item.owner }})
|
||||
</GLink>
|
||||
</template>
|
||||
|
||||
<template v-slot:cell(status)="row">
|
||||
<b>Status: </b><span>{{ row.item.status }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:cell(actions)="row">
|
||||
<InstallationActions
|
||||
class="float-right"
|
||||
:status="row.item.status"
|
||||
@onUninstall="uninstallRepository(row.item)" />
|
||||
</template>
|
||||
</b-table>
|
||||
</b-card>
|
||||
<b-alert v-if="showEmpty" variant="info" show> Currently there are no installing repositories. </b-alert>
|
||||
</GTable>
|
||||
</GCard>
|
||||
|
||||
<BAlert v-if="showEmpty" variant="info" show> Currently there are no installing repositories. </BAlert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import Vue from "vue";
|
||||
import { BAlert } from "bootstrap-vue";
|
||||
|
||||
import { Services } from "../services";
|
||||
|
||||
import InstallationActions from "../RepositoryDetails/InstallationActions.vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
import GLink from "@/components/BaseComponents/GLink.vue";
|
||||
import GCard from "@/components/Common/GCard.vue";
|
||||
import GTable from "@/components/Common/GTable.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BAlert,
|
||||
GCard,
|
||||
GLink,
|
||||
GTable,
|
||||
InstallationActions,
|
||||
},
|
||||
data() {
|
||||
@@ -48,7 +55,20 @@ export default {
|
||||
loading: true,
|
||||
error: null,
|
||||
items: [],
|
||||
fields: ["name", "status", "actions"],
|
||||
fields: [
|
||||
{
|
||||
key: "name",
|
||||
label: "Name",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "Status",
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: "Actions",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -103,8 +123,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.installation-monitor-header {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("RepositoryDetails", () => {
|
||||
|
||||
const localVue = getLocalVue();
|
||||
const pinia = createPinia();
|
||||
const wrapper = shallowMount(Index, {
|
||||
const wrapper = shallowMount(Index as object, {
|
||||
propsData: {
|
||||
repo: {
|
||||
id: "id",
|
||||
@@ -47,7 +47,7 @@ describe("RepositoryDetails", () => {
|
||||
localVue,
|
||||
pinia,
|
||||
});
|
||||
expect(wrapper.find(".loading-message").text()).toBe("Loading repository details...");
|
||||
expect(wrapper.find("loadingspan-stub").attributes("message")).toBe("Loading repository details");
|
||||
await flushPromises();
|
||||
expect(wrapper.findAll(".alert").length).toBe(0);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
|
||||
import { useResourceWatcher } from "@/composables/resourceWatcher";
|
||||
@@ -10,6 +12,8 @@ import { Services } from "../services";
|
||||
import InstallationActions from "./InstallationActions.vue";
|
||||
import InstallationSettings from "./InstallationSettings.vue";
|
||||
import RepositoryTools from "./RepositoryTools.vue";
|
||||
import GTable from "@/components/Common/GTable.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
const props = defineProps({
|
||||
repo: {
|
||||
@@ -31,8 +35,6 @@ const repositoryWatcher = useResourceWatcher(loadInstalledRepositories, {
|
||||
enableBackgroundPolling: false,
|
||||
});
|
||||
|
||||
const repoChecked = "fa fa-check text-success";
|
||||
const repoUnchecked = "fa fa-times text-danger";
|
||||
const repoFields = [
|
||||
{ key: "numeric_revision", label: "Revision" },
|
||||
{ key: "tools", label: "Tools and Versions" },
|
||||
@@ -219,35 +221,31 @@ function stopWatchingRepository() {
|
||||
<b-link :href="repo.repository_url" target="_blank">Show additional details and dependencies.</b-link>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="loading">
|
||||
<span class="fa fa-spinner fa-spin" />
|
||||
<span class="loading-message">Loading repository details...</span>
|
||||
</span>
|
||||
<LoadingSpan v-if="loading" message="Loading repository details" />
|
||||
<div v-else>
|
||||
<b-alert v-if="error" variant="danger" show>
|
||||
{{ error }}
|
||||
</b-alert>
|
||||
<div v-else class="border rounded">
|
||||
<b-table borderless :items="repoTable" :fields="repoFields" class="text-center m-0">
|
||||
<template v-slot:cell(numeric_revision)="data">
|
||||
<span class="font-weight-bold">{{ data.value }}</span>
|
||||
<GTable borderless :items="repoTable" :fields="repoFields" class="text-center m-0">
|
||||
<template v-slot:cell(numeric_revision)="row">
|
||||
<span class="font-weight-bold">{{ row.value }}</span>
|
||||
</template>
|
||||
<template v-slot:cell(tools)="data">
|
||||
<RepositoryTools :tools="data.value" />
|
||||
<template v-slot:cell(tools)="row">
|
||||
<RepositoryTools :tools="row.value" />
|
||||
</template>
|
||||
<template v-slot:cell(profile)="data">
|
||||
{{ data.value ? `+${data.value}` : "-" }}
|
||||
<template v-slot:cell(profile)="row">
|
||||
{{ row.value ? `+${row.value}` : "-" }}
|
||||
</template>
|
||||
<template v-slot:cell(missing_test_components)="data">
|
||||
<span v-if="!data.value" :class="repoChecked" />
|
||||
<span v-else :class="repoUnchecked" />
|
||||
<template v-slot:cell(missing_test_components)="row">
|
||||
<FontAwesomeIcon v-if="!row.value" :icon="faCheck" class="text-success" />
|
||||
<FontAwesomeIcon v-else :icon="faTimes" class="text-danger" />
|
||||
</template>
|
||||
<template v-slot:cell(status)="row">
|
||||
<span v-if="row.item.status">
|
||||
<span v-if="!isFinalState(row.item.status)" class="fa fa-spinner fa-spin" />
|
||||
{{ row.item.status }}
|
||||
</span>
|
||||
<span v-else> - </span>
|
||||
<template v-if="row.item.status">
|
||||
<LoadingSpan v-if="!isFinalState(row.item.status)" :message="row.item.status" />
|
||||
</template>
|
||||
<template v-else> - </template>
|
||||
</template>
|
||||
<template v-slot:cell(actions)="row">
|
||||
<InstallationActions
|
||||
@@ -257,7 +255,8 @@ function stopWatchingRepository() {
|
||||
@onUninstall="uninstallRepository(row.item)"
|
||||
@onReset="resetRepository(row.item)" />
|
||||
</template>
|
||||
</b-table>
|
||||
</GTable>
|
||||
|
||||
<InstallationSettings
|
||||
v-if="showSettings && selectedChangeset"
|
||||
:repo="repo"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLocalVue, mount } from "@vue/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import Categories from "./Categories.vue";
|
||||
import GLink from "@/components/BaseComponents/GLink.vue";
|
||||
|
||||
vi.mock("../services", () => ({
|
||||
Services: class Services {
|
||||
@@ -35,8 +36,11 @@ describe("Categories", () => {
|
||||
toolshedUrl: "toolshedUrl",
|
||||
},
|
||||
localVue,
|
||||
stubs: {
|
||||
LoadingSpan: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper.find(".loading-message").text()).toBe("Loading categories...");
|
||||
expect(wrapper.find("loadingspan-stub").attributes("message")).toBe("Loading categories");
|
||||
});
|
||||
|
||||
it("test categories table", async () => {
|
||||
@@ -46,12 +50,17 @@ describe("Categories", () => {
|
||||
toolshedUrl: "toolshedUrl",
|
||||
},
|
||||
localVue,
|
||||
stubs: {
|
||||
GLink,
|
||||
},
|
||||
});
|
||||
await localVue.nextTick();
|
||||
const links = wrapper.findAll("a");
|
||||
const links = wrapper.findAllComponents(GLink);
|
||||
expect(links.length).toBe(2);
|
||||
expect(links.at(0).text()).toBe("name_0");
|
||||
expect(links.at(1).text()).toBe("name_1");
|
||||
|
||||
expect(links.at(0).text()).toContain("name_0");
|
||||
expect(links.at(1).text()).toContain("name_1");
|
||||
|
||||
const rows = wrapper.findAll("tr");
|
||||
expect(rows.length).toBe(3);
|
||||
const cells = rows.at(1).findAll("td");
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<LoadingSpan v-if="loading" message="Loading categories" />
|
||||
<b-table v-else striped no-sort-reset :items="categories" :fields="fields">
|
||||
<template v-slot:cell(name)="data">
|
||||
<b-link
|
||||
href="javascript:void(0)"
|
||||
role="button"
|
||||
class="font-weight-bold"
|
||||
@click="onCategory(data.value)">
|
||||
{{ data.value }}
|
||||
</b-link>
|
||||
</template>
|
||||
</b-table>
|
||||
<div v-else>
|
||||
<GTable :items="categories" :fields="fields">
|
||||
<template v-slot:cell(name)="data">
|
||||
<GLink
|
||||
href="#"
|
||||
tooltip
|
||||
title="View repositories in this category"
|
||||
@click.prevent="onCategory(data.item.name)">
|
||||
{{ data.item.name }}
|
||||
</GLink>
|
||||
</template>
|
||||
</GTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import Vue from "vue";
|
||||
|
||||
import { Services } from "../services";
|
||||
import { Services } from "@/components/Toolshed/services";
|
||||
|
||||
import GLink from "@/components/BaseComponents/GLink.vue";
|
||||
import GTable from "@/components/Common/GTable.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: { LoadingSpan },
|
||||
components: {
|
||||
GLink,
|
||||
GTable,
|
||||
LoadingSpan,
|
||||
},
|
||||
props: {
|
||||
toolshedUrl: {
|
||||
type: String,
|
||||
@@ -41,8 +45,8 @@ export default {
|
||||
categories: [],
|
||||
fields: [
|
||||
{ key: "name", label: "Category", sortable: true },
|
||||
{ key: "description", sortable: false },
|
||||
{ key: "repositories", sortable: true },
|
||||
{ key: "description", label: "Description", sortable: false },
|
||||
{ key: "repositories", label: "Repositories", sortable: true },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-table id="shed-search-results" striped :items="repositories" :fields="fields">
|
||||
<template v-slot:cell(name)="row">
|
||||
<b-link href="javascript:void(0)" role="button" class="font-weight-bold" @click="row.toggleDetails">
|
||||
{{ row.item.name }}
|
||||
</b-link>
|
||||
<div>{{ row.item.description }}</div>
|
||||
</template>
|
||||
<template v-slot:row-details="row">
|
||||
<RepositoryDetails :repo="row.item" :toolshed-url="toolshedUrl" />
|
||||
</template>
|
||||
</b-table>
|
||||
<div v-if="noResultsFound" class="unavailable-message">No matching repositories found.</div>
|
||||
|
||||
<LoadingSpan v-if="pageLoading" message="Loading repositories" />
|
||||
<GTable v-else id="shed-search-results" :items="repositories" :fields="fields">
|
||||
<template v-slot:cell(name)="{ item, toggleDetails }">
|
||||
<GLink href="#" @click.prevent="toggleDetails">{{ item.name }}</GLink>
|
||||
<div>{{ item.description }}</div>
|
||||
</template>
|
||||
<template v-slot:row-details="{ item }">
|
||||
<RepositoryDetails :repo="item" :toolshed-url="toolshedUrl" />
|
||||
</template>
|
||||
</GTable>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import Vue from "vue";
|
||||
|
||||
<script>
|
||||
import { Services } from "../services";
|
||||
|
||||
import RepositoryDetails from "../RepositoryDetails/Index.vue";
|
||||
import GLink from "@/components/BaseComponents/GLink.vue";
|
||||
import GTable from "@/components/Common/GTable.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
import RepositoryDetails from "@/components/Toolshed/RepositoryDetails/Index.vue";
|
||||
|
||||
const READY = 0;
|
||||
const LOADING = 1;
|
||||
@@ -32,6 +29,8 @@ const COMPLETE = 2;
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpan,
|
||||
GLink,
|
||||
GTable,
|
||||
RepositoryDetails,
|
||||
},
|
||||
props: {
|
||||
@@ -52,10 +51,10 @@ export default {
|
||||
return {
|
||||
repositories: [],
|
||||
fields: [
|
||||
{ key: "name" },
|
||||
{ key: "owner", label: "Owner" },
|
||||
{ key: "times_downloaded", label: "Downloaded" },
|
||||
{ key: "last_updated", label: "Updated" },
|
||||
{ key: "name", label: "Name", sortable: true },
|
||||
{ key: "owner", label: "Owner", sortable: true },
|
||||
{ key: "times_downloaded", label: "Downloaded", sortable: true },
|
||||
{ key: "last_updated", label: "Updated", sortable: true },
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
|
||||
Reference in New Issue
Block a user