Skip to content
Open
4 changes: 4 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,10 @@
"sortZA": "Z-A",
"sortRecent": "Recent",
"sortPopular": "Popular",
"ownership": "Ownership",
"ownershipAll": "All",
"ownershipMyModels": "My models",
"ownershipPublicModels": "Public models",
"selectFrameworks": "Select Frameworks",
"selectProjects": "Select Projects",
"sortingType": "Sorting Type",
Expand Down
1 change: 1 addition & 0 deletions src/platform/assets/components/AssetBrowserModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<template #contentFilter>
<AssetFilterBar
:assets="categoryFilteredAssets"
:all-assets="fetchedAssets"
@filter-change="updateFilters"
/>
</template>
Expand Down
48 changes: 38 additions & 10 deletions src/platform/assets/components/AssetFilterBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
data-component-id="asset-filter-base-models"
@update:model-value="handleFilterChange"
/>

<SingleSelect
v-if="hasMutableAssets"
v-model="ownership"
:label="$t('assetBrowser.ownership')"
:options="ownershipOptions"
class="min-w-42"
data-component-id="asset-filter-ownership"
@update:model-value="handleFilterChange"
/>
</div>

<div class="flex items-center" data-component-id="asset-filter-bar-right">
Expand All @@ -44,9 +54,9 @@
</div>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'

import MultiSelect from '@/components/input/MultiSelect.vue'
import SingleSelect from '@/components/input/SingleSelect.vue'
Expand All @@ -55,12 +65,6 @@ import { t } from '@/i18n'
import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'

export interface FilterState {
fileFormats: string[]
baseModels: string[]
sortBy: string
}

const SORT_OPTIONS = [
{ name: t('assetBrowser.sortRecent'), value: 'recent' },
{ name: t('assetBrowser.sortAZ'), value: 'name-asc' },
Expand All @@ -71,17 +75,40 @@ type SortOption = (typeof SORT_OPTIONS)[number]['value']

const sortOptions = [...SORT_OPTIONS]

const { assets = [] } = defineProps<{
const OWNERSHIP_OPTIONS = [
{ name: t('assetBrowser.ownershipAll'), value: 'all' },
{ name: t('assetBrowser.ownershipMyModels'), value: 'my-models' },
{ name: t('assetBrowser.ownershipPublicModels'), value: 'public-models' }
] as const

type OwnershipOption = (typeof OWNERSHIP_OPTIONS)[number]['value']

const ownershipOptions = [...OWNERSHIP_OPTIONS]

export interface FilterState {
fileFormats: string[]
baseModels: string[]
sortBy: string
ownership: OwnershipOption
}

const { assets = [], allAssets = [] } = defineProps<{
assets?: AssetItem[]
allAssets?: AssetItem[]
}>()

const fileFormats = ref<SelectOption[]>([])
const baseModels = ref<SelectOption[]>([])
const sortBy = ref<SortOption>('recent')
const ownership = ref<OwnershipOption>('all')

const { availableFileFormats, availableBaseModels } =
useAssetFilterOptions(assets)

const hasMutableAssets = computed(() =>
allAssets.some((asset) => asset.is_immutable === false)
)

const emit = defineEmits<{
filterChange: [filters: FilterState]
}>()
Expand All @@ -90,7 +117,8 @@ function handleFilterChange() {
emit('filterChange', {
fileFormats: fileFormats.value.map((option: SelectOption) => option.value),
baseModels: baseModels.value.map((option: SelectOption) => option.value),
sortBy: sortBy.value
sortBy: sortBy.value,
ownership: ownership.value
})
}
</script>
13 changes: 12 additions & 1 deletion src/platform/assets/composables/useAssetBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ function filterByBaseModels(models: string[]) {
}
}

function filterByOwnership(ownership: string) {
return (asset: AssetItem) => {
if (ownership === 'all') return true
if (ownership === 'my-models') return asset.is_immutable === false
if (ownership === 'public-models') return asset.is_immutable === true
return true
}
}

type AssetBadge = {
label: string
type: 'type' | 'base' | 'size'
Expand Down Expand Up @@ -65,7 +74,8 @@ export function useAssetBrowser(
const filters = ref<FilterState>({
sortBy: 'recent',
fileFormats: [],
baseModels: []
baseModels: [],
ownership: 'all'
})

// Transform API asset to display asset
Expand Down Expand Up @@ -176,6 +186,7 @@ export function useAssetBrowser(
const filtered = searchFiltered.value
.filter(filterByFileFormats(filters.value.fileFormats))
.filter(filterByBaseModels(filters.value.baseModels))
.filter(filterByOwnership(filters.value.ownership))

const sortedAssets = [...filtered]
sortedAssets.sort((a, b) => {
Expand Down
8 changes: 7 additions & 1 deletion src/platform/assets/fixtures/ui-mock-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,15 @@ export function createAssetWithoutUserMetadata() {
return asset
}

export function createAssetWithSpecificExtension(extension: string) {
export function createAssetWithSpecificExtension(
extension: string,
isImmutable?: boolean
) {
const asset = createMockAssets(1)[0]
asset.name = `test-model.${extension}`
if (isImmutable !== undefined) {
asset.is_immutable = isImmutable
}
return asset
}

Expand Down
94 changes: 92 additions & 2 deletions tests-ui/platform/assets/components/AssetFilterBar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,10 @@
await nextTick()

// Update sort
const sortSelect = wrapper.findComponent({ name: 'SingleSelect' })
await sortSelect.vm.$emit('update:modelValue', 'popular')
const sortSelect = wrapper
.findAllComponents({ name: 'SingleSelect' })
.find((component) => component.props('label') === 'assetBrowser.sortBy')
await sortSelect!.vm.$emit('update:modelValue', 'popular')

await nextTick()

Expand Down Expand Up @@ -247,5 +249,93 @@
const multiSelects = wrapper.findAllComponents({ name: 'MultiSelect' })
expect(multiSelects).toHaveLength(0)
})

it('hides ownership filter when no mutable assets', () => {
const assets = [
createAssetWithSpecificExtension('safetensors', true) // immutable
]
const wrapper = mountAssetFilterBar({ assets })

const ownershipSelects = wrapper
.findAllComponents({ name: 'SingleSelect' })
.filter(
(component) => component.props('label') === 'assetBrowser.ownership'
)

expect(ownershipSelects).toHaveLength(0)
})

it('shows ownership filter when mutable assets exist', () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets })

const ownershipSelects = wrapper
.findAllComponents({ name: 'SingleSelect' })
.filter(
(component) => component.props('label') === 'assetBrowser.ownership'
)

expect(ownershipSelects).toHaveLength(1)

Check failure on line 280 in tests-ui/platform/assets/components/AssetFilterBar.test.ts

View workflow job for this annotation

GitHub Actions / test

tests-ui/platform/assets/components/AssetFilterBar.test.ts > AssetFilterBar > Conditional Filter Visibility > shows ownership filter when mutable assets exist

AssertionError: expected [] to have a length of 1 but got +0 - Expected + Received - 1 + 0 ❯ tests-ui/platform/assets/components/AssetFilterBar.test.ts:280:32

Check failure on line 280 in tests-ui/platform/assets/components/AssetFilterBar.test.ts

View workflow job for this annotation

GitHub Actions / test

tests-ui/platform/assets/components/AssetFilterBar.test.ts > AssetFilterBar > Conditional Filter Visibility > shows ownership filter when mutable assets exist

AssertionError: expected [] to have a length of 1 but got +0 - Expected + Received - 1 + 0 ❯ tests-ui/platform/assets/components/AssetFilterBar.test.ts:280:32

Check failure on line 280 in tests-ui/platform/assets/components/AssetFilterBar.test.ts

View workflow job for this annotation

GitHub Actions / test

tests-ui/platform/assets/components/AssetFilterBar.test.ts > AssetFilterBar > Conditional Filter Visibility > shows ownership filter when mutable assets exist

AssertionError: expected [] to have a length of 1 but got +0 - Expected + Received - 1 + 0 ❯ tests-ui/platform/assets/components/AssetFilterBar.test.ts:280:32
})

it('shows ownership filter when mixed assets exist', () => {
const assets = [
createAssetWithSpecificExtension('safetensors', true), // immutable
createAssetWithSpecificExtension('ckpt', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets })

const ownershipSelects = wrapper
.findAllComponents({ name: 'SingleSelect' })
.filter(
(component) => component.props('label') === 'assetBrowser.ownership'
)

expect(ownershipSelects).toHaveLength(1)

Check failure on line 296 in tests-ui/platform/assets/components/AssetFilterBar.test.ts

View workflow job for this annotation

GitHub Actions / test

tests-ui/platform/assets/components/AssetFilterBar.test.ts > AssetFilterBar > Conditional Filter Visibility > shows ownership filter when mixed assets exist

AssertionError: expected [] to have a length of 1 but got +0 - Expected + Received - 1 + 0 ❯ tests-ui/platform/assets/components/AssetFilterBar.test.ts:296:32

Check failure on line 296 in tests-ui/platform/assets/components/AssetFilterBar.test.ts

View workflow job for this annotation

GitHub Actions / test

tests-ui/platform/assets/components/AssetFilterBar.test.ts > AssetFilterBar > Conditional Filter Visibility > shows ownership filter when mixed assets exist

AssertionError: expected [] to have a length of 1 but got +0 - Expected + Received - 1 + 0 ❯ tests-ui/platform/assets/components/AssetFilterBar.test.ts:296:32

Check failure on line 296 in tests-ui/platform/assets/components/AssetFilterBar.test.ts

View workflow job for this annotation

GitHub Actions / test

tests-ui/platform/assets/components/AssetFilterBar.test.ts > AssetFilterBar > Conditional Filter Visibility > shows ownership filter when mixed assets exist

AssertionError: expected [] to have a length of 1 but got +0 - Expected + Received - 1 + 0 ❯ tests-ui/platform/assets/components/AssetFilterBar.test.ts:296:32
})
})

describe('Ownership Filter', () => {
it('emits ownership filter changes', async () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets })

const ownershipSelect = wrapper
.findAllComponents({ name: 'SingleSelect' })
.find(
(component) => component.props('label') === 'assetBrowser.ownership'
)

await ownershipSelect!.vm.$emit('update:modelValue', 'my-models')

Check failure on line 313 in tests-ui/platform/assets/components/AssetFilterBar.test.ts

View workflow job for this annotation

GitHub Actions / test

tests-ui/platform/assets/components/AssetFilterBar.test.ts > AssetFilterBar > Ownership Filter > emits ownership filter changes

TypeError: Cannot read properties of undefined (reading 'vm') ❯ tests-ui/platform/assets/components/AssetFilterBar.test.ts:313:30

Check failure on line 313 in tests-ui/platform/assets/components/AssetFilterBar.test.ts

View workflow job for this annotation

GitHub Actions / test

tests-ui/platform/assets/components/AssetFilterBar.test.ts > AssetFilterBar > Ownership Filter > emits ownership filter changes

TypeError: Cannot read properties of undefined (reading 'vm') ❯ tests-ui/platform/assets/components/AssetFilterBar.test.ts:313:30

Check failure on line 313 in tests-ui/platform/assets/components/AssetFilterBar.test.ts

View workflow job for this annotation

GitHub Actions / test

tests-ui/platform/assets/components/AssetFilterBar.test.ts > AssetFilterBar > Ownership Filter > emits ownership filter changes

TypeError: Cannot read properties of undefined (reading 'vm') ❯ tests-ui/platform/assets/components/AssetFilterBar.test.ts:313:30
await nextTick()

const emitted = wrapper.emitted('filterChange')
expect(emitted).toHaveLength(1)

const filterState = emitted![0][0] as FilterState
expect(filterState.ownership).toBe('my-models')
})

it('ownership filter defaults to "all"', async () => {
const assets = [
createAssetWithSpecificExtension('safetensors', false) // mutable
]
const wrapper = mountAssetFilterBar({ assets })

const sortSelect = wrapper
.findAllComponents({ name: 'SingleSelect' })
.find((component) => component.props('label') === 'assetBrowser.sortBy')

await sortSelect!.vm.$emit('update:modelValue', 'recent')
await nextTick()

const emitted = wrapper.emitted('filterChange')
const filterState = emitted![0][0] as FilterState
expect(filterState.ownership).toBe('all')
})
})
})
Loading
Loading