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

feat: knn project search ui #5651

Merged
merged 14 commits into from
Nov 29, 2024
171 changes: 147 additions & 24 deletions packages/client/hmi-client/src/components/home/tera-project-table.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,51 @@
<template>
<DataTable :value="projects" dataKey="id" :rowsPerPageOptions="[10, 20, 50]" scrollable scrollHeight="45rem">
<!--The pt wrapper styling enables the table header to stick with the project search bar-->
<DataTable
ref="projectTableRef"
:value="projectsWithKnnMatches"
:rows="numberOfRows"
:rows-per-page-options="[10, 20, 30, 40, 50]"
:pt="{ wrapper: { style: { overflow: 'none' } } }"
data-key="id"
paginator
@page="getProjectAssets"
>
<Column
v-for="(col, index) in selectedColumns"
:field="col.field"
:header="col.header"
:sortable="!['stats', 'score'].includes(col.field)"
:key="index"
:style="`width: ${getColumnWidth(col.field)}%`"
>
<template #body="{ data }">
<template v-if="col.field === 'score'">
{{ Math.round((data.metadata?.score ?? 0) * 100) + '%' }}
</template>
<template v-if="col.field === 'name'">
<a class="project-title-link" @click.stop="emit('open-project', data.id)">
{{ data.name }}
</a>
<a @click.stop="emit('open-project', data.id)">{{ data.name }}</a>
<ul>
<li
v-for="(asset, index) in data.projectAssets.slice(0, data.showMore ? data.projectAssets.length : 3)"
class="flex align-center gap-2"
:key="index"
>
shawnyama marked this conversation as resolved.
Show resolved Hide resolved
<tera-asset-icon :assetType="asset.assetType" />
<span v-html="highlight(asset.assetName, searchQuery)" />
</li>
</ul>
<Button
v-if="data.projectAssets.length > 3"
class="p-2 mt-2"
:label="data.showMore ? 'Show less' : 'Show more'"
text
size="small"
@click="data.showMore = !data.showMore"
/>
</template>
<template v-else-if="col.field === 'description'">
<tera-show-more-text :text="data.description" :lines="1" />
<p v-if="data.snippet" class="mt-2" v-html="data.snippet" />
</template>
<tera-show-more-text v-else-if="col.field === 'description'" :text="data.description" :lines="1" />
<template v-if="col.field === 'userName'">
{{ data.userName ?? '--' }}
</template>
Expand Down Expand Up @@ -68,21 +96,41 @@
</template>

<script setup lang="ts">
import { isEmpty } from 'lodash';
import { ref, watch } from 'vue';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import TeraShowMoreText from '@/components/widgets/tera-show-more-text.vue';
import { formatDdMmmYyyy } from '@/utils/date';
import DatasetIcon from '@/assets/svg/icons/dataset.svg?component';
import { Project } from '@/types/Types';
import { AssetType, Project } from '@/types/Types';
import type { PageState } from 'primevue/paginator';
import * as ProjectService from '@/services/project';
import { highlight } from '@/utils/text';
import Button from 'primevue/button';
import TeraProjectMenu from './tera-project-menu.vue';
import TeraAssetIcon from '../widgets/tera-asset-icon.vue';

interface ProjectWithKnnSnippet extends Project {
snippet?: string;
showMore?: boolean;
}

defineProps<{
const props = defineProps<{
projects: Project[];
selectedColumns: { field: string; header: string }[];
searchQuery: string;
}>();

const emit = defineEmits(['open-project']);

const projectTableRef = ref();
const projectsWithKnnMatches = ref<ProjectWithKnnSnippet[]>([]);
const numberOfRows = ref(20);

let pageState: PageState = { page: 0, rows: numberOfRows.value, first: 0 };
let prevSearchQuery = '';

function formatStat(data, key) {
const stat = data?.[key];
return key === 'contributor-count' ? parseInt(stat ?? '1', 10) : parseInt(stat ?? '0', 10);
Expand All @@ -92,18 +140,46 @@ function formatStatTooltip(stat, displayName) {
return `${stat} ${displayName}${stat === 1 ? '' : 's'}`;
}

function getColumnWidth(columnField: string) {
switch (columnField) {
case 'description':
return 40;
case 'name':
return 40;
case 'score':
return 5;
default:
return 100;
}
async function getProjectAssets(event: PageState = pageState) {
pageState = event; // Save the current page state so we still know it when the watch is triggered
if (isEmpty(props.searchQuery)) return;

const { rows, first } = event;
const searchQuery = props.searchQuery.toLowerCase().trim();

// Just fetch the asset data we are seeing in the current page
projectsWithKnnMatches.value.slice(first, first + rows).forEach(async (project) => {
// If assets were fetched before from when we were on that page don't redo it
if (!isEmpty(project.projectAssets) && prevSearchQuery === searchQuery) return;

const projectWithAssets = (await ProjectService.get(project.id)) as ProjectWithKnnSnippet | null;
if (!projectWithAssets) return;

project.projectAssets = projectWithAssets.projectAssets.filter(
(asset) => asset.assetName.toLowerCase().includes(searchQuery) && asset.assetType !== AssetType.Simulation // Simulations don't have names
shawnyama marked this conversation as resolved.
Show resolved Hide resolved
);
project.showMore = false;
project.snippet = project.description?.toLowerCase().includes(searchQuery)
? highlight(project.description, searchQuery)
: undefined;
});

prevSearchQuery = searchQuery;
}

watch(
() => props.projects,
() => {
// Reset the page to 0 when a new search is performed
if (pageState.page !== 0) {
const firstPageButton = projectTableRef.value.$el.querySelector('.p-paginator-first');
firstPageButton?.dispatchEvent(new MouseEvent('click'));
}
shawnyama marked this conversation as resolved.
Show resolved Hide resolved
projectsWithKnnMatches.value = props.projects;
getProjectAssets();
},
{ immediate: true }
);
</script>

<style scoped>
Expand All @@ -123,8 +199,50 @@ function getColumnWidth(columnField: string) {
}

.p-datatable {
border: 1px solid var(--surface-border-light);
border-radius: var(--border-radius);
/* Now the table header sticks along with the project search bar */
&:deep(thead) {
top: 105px;
z-index: 1;
}
}

:deep(.p-paginator-bottom) {
position: sticky;
bottom: 0;
outline: 1px solid var(--surface-border-light);
}

:deep(.p-paginator) {
border-radius: 0;
padding: var(--gap-2) var(--gap-4);
}

.p-datatable:deep(ul) {
margin-top: var(--gap-4);
color: var(--text-color-primary);
display: flex;
flex-direction: column;
gap: var(--gap-2);
font-size: var(--font-caption);
}

.p-datatable:deep(li > span) {
text-overflow: ellipsis;
display: block;
overflow: hidden;
max-width: 20vw;
}

.p-datatable:deep(p) {
color: var(--text-color-primary);
max-width: 22vw;
text-overflow: ellipsis;
display: block;
overflow: hidden;
}

.p-datatable:deep(.highlight) {
font-weight: var(--font-weight-semibold);
}

.p-datatable:deep(.p-datatable-tbody > tr > td),
Expand All @@ -136,7 +254,9 @@ function getColumnWidth(columnField: string) {
}

.p-datatable:deep(.p-datatable-thead > tr > th) {
padding: 1rem 0.5rem;
padding-left: var(--gap-5);
padding-top: var(--gap-3);
padding-bottom: var(--gap-3);
background-color: var(--surface-ground);
}

Expand All @@ -145,8 +265,8 @@ function getColumnWidth(columnField: string) {
}

.p-datatable:deep(.p-datatable-tbody > tr > td) {
padding-left: var(--gap-5);
color: var(--text-color-secondary);
padding: 0.5rem;
max-width: 32rem;
}

Expand All @@ -165,7 +285,10 @@ function getColumnWidth(columnField: string) {
.p-datatable:deep(.p-datatable-tbody > tr > td > a) {
color: var(--text-color-primary);
font-weight: var(--font-weight-semibold);
cursor: pointer;
text-overflow: ellipsis;
display: block;
overflow: hidden;
max-width: 20vw;
}

.p-datatable:deep(.p-datatable-tbody > tr > td > a:hover) {
Expand Down
Loading