Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 58 additions & 39 deletions components/card/resource.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<script setup lang="ts">
import {modelsv2Status, v2DataClass, type v2GenericResource, v2ResourceVariant,} from '~/composables/aruna_api_json';
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger,} from "@/components/ui/tooltip"

import {ResourceVariant, VisibilityClass} from "~/types/aruna-v3-enums";


/* ----- PROPERTIES ----- */
interface ResourceCardProps {
resource: v2GenericResource
resource: Resource
}

const {resource} = defineProps<ResourceCardProps>()
const entry = toSearchResult(resource)

/* ----- END PROPERTIES ----- */

Expand All @@ -17,72 +19,88 @@ function displayDescription(description: string): string {
}
return description
}
function displayDataClass(dataClass: v2DataClass): string {
switch (dataClass) {
case v2DataClass.DATA_CLASS_PUBLIC: return 'Public'
case v2DataClass.DATA_CLASS_PRIVATE: return 'Private'
default: return 'Unspecified'

function displayVisbility(visibility: VisibilityClass): string {
switch (visibility) {
case VisibilityClass.Public:
return 'Public';
case VisibilityClass.PublicMetadata:
return 'Public Metadata';
case VisibilityClass.Private:
return 'Private';
default:
return 'Unspecified'
}
}
function dataClassCss(dataClass: v2DataClass): string {
switch (dataClass) {
case v2DataClass.DATA_CLASS_PUBLIC: return 'border-green-600 text-green-600'
case v2DataClass.DATA_CLASS_PRIVATE: return 'border-red-600/80 text-red-600/80'
case v2DataClass.DATA_CLASS_CONFIDENTIAL: return 'border-white text-white'
case v2DataClass.DATA_CLASS_WORKSPACE: return 'border-indigo-600 text-indigo-600'
default: return 'border-aruna-text/50 text-aruna-text/50'

function visibilityCss(visibility: VisibilityClass): string {
switch (visibility) {
case VisibilityClass.Public:
return 'border-green-600 text-green-600';
case VisibilityClass.PublicMetadata:
return 'border-white text-white';
case VisibilityClass.Private:
return 'border-red-600/80 text-red-600/80';
default:
return 'border-aruna-text/50 text-aruna-text/50'
}
}
</script>

<template>
<div v-if="entry"
class="flex flex-auto my-3 p-4 gap-x-4 bg-aruna-bg/90 border border-l-4 border-l-aruna-highlight shadow-sm rounded-md border-aruna-text/50"
:class="{'border-l-destructive': entry.object_status === modelsv2Status.STATUS_DELETED}">
<div
class="flex flex-auto my-3 p-4 gap-x-4 bg-aruna-bg/90 border border-l-4 border-l-aruna-highlight shadow-sm rounded-md border-aruna-text/50"
:class="{'border-l-destructive': resource.deleted}">
<div class="flex flex-col basis-1/3 gap-y-3 border-aruna-text/50">
<!-- Name and ID display with links -->
<NuxtLink :to="`/objects/${entry.id}`"
class="text-aruna-highlight font-bold">
<h3>{{ entry.name }}</h3>
</NuxtLink>
<!-- Name and ID display -->
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<NuxtLink :to="`/objects/${resource.id}`"
class="text-aruna-highlight font-bold">
<h3>{{ resource.name }}</h3>
</NuxtLink>
</TooltipTrigger>
<TooltipContent>
<p>{{ resource.id }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

<NuxtLink :to="`/objects/${entry.id}`"
class="text-sm text-aruna-text-accent font-bold">
<h4>{{ entry.id }}</h4>
</NuxtLink>
<!-- END Name and ID display with links -->
<h4>{{ resource.type === "Resource" ? resource.variant : resource.type }}</h4>
<!-- END Name and ID display -->

<!-- Badges -->
<div class="flex flex-col md:flex-row mt-2 gap-x-2 gap-y-2">
<!-- DataClass Badge -->
<Badge :class="cn('px-2 py-1 bg-transparent hover:bg-transparent border', dataClassCss(entry.data_class))">
{{ displayDataClass(entry.data_class) }}
<Badge :class="cn('px-2 py-1 bg-transparent hover:bg-transparent border', visibilityCss(resource.data_class))">
{{ displayVisbility(resource.visibility) }}
</Badge>
<!-- END DataClass -->
<!-- Stats Badge -->
<Badge v-if="entry.variant !== v2ResourceVariant.RESOURCE_VARIANT_OBJECT"
<Badge v-if="resource.variant !== ResourceVariant.Object"
class="px-2 py-1 bg-transparent hover:bg-transparent border border-aruna-text text-aruna-text">
Children: {{ entry.stats.count }}
Children: {{ resource.count }}
</Badge>
<Badge class="px-2 py-1 bg-transparent hover:bg-transparent border border-aruna-text text-aruna-text">
{{ entry.stats.size ? formatBytes(+entry.stats.size) : "N/A Bytes" }}
<Badge v-if="resource.variant === ResourceVariant.Object"
class="px-2 py-1 bg-transparent hover:bg-transparent border border-aruna-text text-aruna-text">
{{ resource.content_len ? formatBytes(+resource.content_len) : "N/A Bytes" }}
</Badge>
<!-- END Stats Badge -->
</div>
<!-- END Badges -->
</div>

<div class="mx-2 grow flex-col basis-2/3">
<h4 class="text-sm my-3 uppercase text-aruna-text-accent">Description</h4>
<p v-if="entry.description.length > 0" class="mt-2 text-aruna-text">
{{ displayDescription(entry.description) }}
<p v-if="resource.description.length > 0" class="mt-2 text-aruna-text">
{{ displayDescription(resource.description) }}
</p>

<hr v-if="entry.key_values.length > 0" class="my-4 text-aruna-text "/>
<hr v-if="resource.labels.length > 0" class="my-4 text-aruna-text "/>

<!-- Basic Label Display -->
<div class="flex flex-row flex-wrap">
<div v-for="label in entry.key_values" class="flex flex-row my-1 me-2 last:me-0">
<div v-for="label in resource.labels" class="flex flex-row my-1 me-2 last:me-0">
<span
:class="{ 'me-2 rounded-md': label.value === '' || label.value === undefined, 'rounded-l-md': label.value && label.value.length > 0 }"
class="text-xs font-medium truncate max-w-60 whitespace-nowrap items-center gap-x-1.5 py-1.5 px-3 rounded-l-md text-aruna-highlight border border-aruna-highlight">
Expand All @@ -93,6 +111,7 @@ function dataClassCss(dataClass: v2DataClass): string {
{{ label.value }}</span>
</div>
</div>

</div>
</div>
</template>
166 changes: 166 additions & 0 deletions components/custom-ui/SearchDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<script setup lang="ts">

import {IconLoader2, IconSearch} from "@tabler/icons-vue";
import {GenericNodeType, ResourceVariant} from "~/types/aruna-v3-enums";

const DEFAULT_LIMIT = useRuntimeConfig().public.searchDialogLimit || 10;

/* ----- Properties ----- */
interface SearchDialogProps {
limitResults?: number,
filter?: string
}

const props = defineProps<SearchDialogProps>()
const limitResults = props.limitResults || DEFAULT_LIMIT
const filter = props.filter
/* ----- End Properties ----- */

const router = useRouter()

const queryInput = ref('')
const fetching = ref(false)
const response: Ref<SearchResponse | undefined | void> = ref(undefined)
const dialogOpen = ref(false)

const resources: ComputedRef<SplitResources | undefined> = computed(() => splitResources(response.value?.resources));

type SplitResources = {
projects: Resource[],
folders: Resource[],
objects: Resource[],
other: any[],
}

function handleDialogOpenChange() {
dialogOpen.value = !dialogOpen.value
}

function openDialog(): void {
queryInput.value = ''; // reset the query when the dialog is opened again
dialogOpen.value = true;
}

// Debounce input to decrease number of requests
watch(queryInput, () => {
// Empty input only clears results
if (!queryInput.value) {
response.value = undefined
return
}

fetching.value = true
response.value = undefined
debouncedInput()
})

const debouncedInput = debounce(async () => await searchResources(queryInput.value), 250)

async function searchResources(query: string): Promise<void> {
// No need to search with empty input
if (!query) {
response.value = undefined
return
}

response.value = await $fetch<SearchResponse>('/api/v3/search', {
query: {
query: query,
limit: limitResults,
filter: filter
}
}).catch(error => {
console.log(error)
return
})
fetching.value = false
dialogOpen.value = true
}

function splitResources(resources: (Resource & { [type: string]: GenericNodeType })[]): SplitResources {
var result: SplitResources = {
projects: [],
folders: [],
objects: [],
other: []
}
if (resources) {
for (const resource of resources) {
if (resource.type === GenericNodeType.Resource) {
switch (resource.variant) {
case ResourceVariant.Project:
result.projects.push(resource)
break;
case ResourceVariant.Folder:
result.folders.push(resource)
break;
case ResourceVariant.Object:
result.objects.push(resource)
break;
}
} else {
result.other.push(resource)
}
}
}
return result
}
</script>

<template>
<div class="relative flex">
<Button @click="openDialog()"
class="py-2 px-3 inline-flex justify-center items-center bg-transparent border border-aruna-highlight rounded-sm text-aruna-highlight hover:text-aruna-text-accent hover:bg-aruna-highlight disabled:opacity-50 disabled:pointer-events-none focus:outline-none focus:ring-1 focus:ring-aruna-highlight cursor-pointer">
<IconSearch class="mr-1 h-4 w-4"/>
Search
</Button>

<!-- COMMAND DIALOG -->
<CommandDialog class="rounded-sm border" :open="dialogOpen" @update:open="handleDialogOpenChange">
<CommandInput placeholder="Search for resources"
class="border-0 focus:border-0 outline-0 focus:outline-0 focus:ring-0"
@input="(event) => queryInput = event.target.value"/>
<CommandList class="gap-y-2">
<CommandEmpty v-if="queryInput && fetching"
class="flex items-center justify-center gap-x-4">
Fetching results
<IconLoader2 class="mr-2 animate-spin"/>
</CommandEmpty>
<CommandEmpty v-if="queryInput && !fetching">
No results found.
</CommandEmpty>
<CommandGroup heading="Projects">
<CommandItem v-for="resource in resources?.projects"
:value="resource"
@select="router.push('/projects/' + resource.id)"> <!-- Dummy, replace with proper path later -->
{{ resource.name }}
</CommandItem>
</CommandGroup>
<CommandGroup heading="Folders">
<CommandItem v-for="resource in resources?.folders"
:value="resource"
@select="router.push('/objects/' + resource.id)">
{{ resource.name }}
</CommandItem>
</CommandGroup>
<CommandGroup heading="Objects">
<CommandItem v-for="resource in resources?.objects"
:value="resource"
@select="router.push('/objects/' + resource.id)">
{{ resource.name }}
</CommandItem>
</CommandGroup>
<CommandGroup heading="Others">
<CommandItem v-for="resource in resources?.other"
:value="resource">
{{ resource.type + ": " + resource.name }}
</CommandItem>
</CommandGroup>
</CommandList>
<Button v-if="response?.expected_hits > limitResults">
<NuxtLink :href="'/explore?q=' + queryInput">Show all results</NuxtLink>
</Button>
</CommandDialog>
<!-- END COMMAND DIALOG-->
</div>
</template>
8 changes: 2 additions & 6 deletions components/custom-ui/dashboard/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import Events from "~/components/custom-ui/dashboard/Events.vue";
import Profile from "~/components/custom-ui/user/Profile.vue";
import Tokens from "~/components/custom-ui/user/Tokens.vue";
import Proxies from "~/components/custom-ui/user/Proxies.vue";
import SearchDialog from "~/components/custom-ui/SearchDialog.vue";
import Licences from "~/components/custom-ui/dashboard/licenses/Licenses.vue";

import type {User} from '~/composables/api_wrapper'
Expand Down Expand Up @@ -349,12 +350,7 @@ EventBus.on('spinStop', () => spinBaby.value = false)
<Skeleton class="h-auto w-[300px]"/>
</template>
</ClientOnly>
<div class="relative flex">
<IconSearch class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground"/>
<Input type="search"
placeholder="Search Resources"
class="w-[400px] appearance-none bg-background pl-8 shadow-none "/>
</div>
<SearchDialog filter="variant IN [0,1,2]"/>
</div>
<DropdownMenu>
<ClientOnly fallbackTag="span">
Expand Down
1 change: 0 additions & 1 deletion components/custom-ui/dashboard/EventList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import {ScrollArea} from '@/components/ui/scroll-area'
import {cn} from '@/utils/shadcn'
import {Badge} from '@/components/ui/badge'
import {decodeTime} from 'ulid'
import {formatDate} from '~/composables/utils'
import {formatDistanceToNow} from "date-fns";

Expand Down
10 changes: 7 additions & 3 deletions components/search_results.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
<script setup lang="ts">
import type {v2GenericResource} from '@/composables/aruna_api_json';

interface SearchResultListProps {
resources: v2GenericResource[]
resources: Resource[],
fetching: boolean,
}
const {resources} = defineProps<SearchResultListProps>()
</script>

<template>
<div v-if="resources.length == 0">
<div v-if="fetching">
<h2 class="text-aruna-text-accent font-bold text-center">Fetching Results</h2>
</div>

<div v-if="resources.length == 0 && !fetching">
<h2 class="text-aruna-text-accent font-bold text-center">No results found</h2>
</div>

Expand Down
2 changes: 1 addition & 1 deletion composables/api_wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export type CreateGroupResponse = paths["/api/v3/groups"]["post"]["responses"][2
// Info
export type GetEventsResponse = paths["/api/v3/info/events"]["get"]["responses"][200]["content"]["application/json"];
export type GetStatsResponse = paths["/api/v3/info/stats"]["get"]["responses"][200]["content"]["application/json"];
export type SearchResponse = paths["/api/v3/info/search"]["get"]["responses"][200]["content"]["application/json"];
export type SearchResponse = paths["/api/v3/info/search"]["get"]["responses"][200]["content"]["applicatiown/json"];

// License
export type CreateLicensesResponse = paths["/api/v3/license"]["post"]["responses"][200]["content"]["application/json"];
Expand Down
3 changes: 2 additions & 1 deletion nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ export default defineNuxtConfig({
hidePeriod: 60 * 60 * 24, // Hide banner per default for one day
validFrom: '', // Some valid date string
validTo: '', // Some valid date string
}
},
searchDialogLimit: 10
}
},

Expand Down
Loading