frontend/src/components/NamespacesList.vue
<!-- NamespacesList.vue -->
<script lang="ts">
import { defineComponent, ref, computed, PropType, onBeforeUnmount } from 'vue';
import { KubernetesCluster, KubernetesNamespace } from '../types/kubernetes';
import { formatAge } from '../lib/format';
import SearchBar from './SearchBar.vue';
import NamespacesCreateMethodSelector from './NamespacesCreateMethodSelector.vue';
import NamespacesCreateGuided from './NamespacesCreateGuided.vue';
import NamespacesCreateYaml from './NamespacesCreateYaml.vue';
import NamespacesEdit from './NamespacesEdit.vue';
export default defineComponent({
name: 'NamespacesList',
components: {
SearchBar,
NamespacesCreateMethodSelector,
NamespacesCreateGuided,
NamespacesCreateYaml,
NamespacesEdit
},
props: {
selectedCluster: {
type: Object as PropType<KubernetesCluster>,
required: false,
default: null
},
namespaces: {
type: Array as PropType<KubernetesNamespace[]>,
required: false,
default: () => []
}
},
emits: ['namespace-selected', 'delete-namespace', 'create-namespace'],
setup(props, { emit }) {
// State
const searchQuery = ref('');
const selectedNamespace = ref<KubernetesNamespace | null>(null);
const selectedContextNamespace = ref<KubernetesNamespace | null>(null);
const showMenu = ref(false);
const menuPosition = ref({ x: 0, y: 0 });
const showMethodSelector = ref(false);
const showCreateGuided = ref(false);
const showCreateYaml = ref(false);
const showEditYaml = ref(false);
// Computed properties
const filteredNamespaces = computed(() => {
if (!searchQuery.value) {
return props.namespaces;
}
const query = searchQuery.value.toLowerCase();
const nameSpecificMatch = query.match(/^name:(.+)$/);
if (nameSpecificMatch) {
const nameQuery = nameSpecificMatch[1].trim();
return props.namespaces.filter(namespace => {
const name = namespace.metadata.name.toLowerCase();
return name.includes(nameQuery);
});
}
const statusSpecificMatch = query.match(/^status:(.+)$/);
if (statusSpecificMatch) {
const statusQuery = statusSpecificMatch[1].trim();
return props.namespaces.filter(namespace => {
const status = namespace.status?.phase?.toLowerCase() || '';
return status.includes(statusQuery);
});
}
const labelSpecificMatch = query.match(/^label:(.+)$/);
if (labelSpecificMatch) {
const labelQuery = labelSpecificMatch[1].trim();
return props.namespaces.filter(namespace => {
const labels = namespace.metadata.labels || {};
for (const key in labels) {
const value = labels[key].toLowerCase();
const keyLower = key.toLowerCase();
// Check for key=value format
if (labelQuery.includes('=')) {
const [queryKey, queryValue] = labelQuery.split('=');
if (keyLower === queryKey.trim().toLowerCase() &&
value.includes(queryValue.trim().toLowerCase())) {
return true;
}
}
// Check for just key
else if (keyLower.includes(labelQuery)) {
return true;
}
}
return false;
});
}
// Default behavior: search only in namespace names
return props.namespaces.filter(namespace => {
const name = namespace.metadata.name.toLowerCase();
return name.includes(query);
});
});
// Methods
const hideContextMenu = () => {
showMenu.value = false;
document.removeEventListener('click', hideContextMenu);
};
const showContextMenu = (event: MouseEvent, namespace: KubernetesNamespace) => {
event.preventDefault();
menuPosition.value = {
x: event.clientX,
y: event.clientY
};
selectedContextNamespace.value = namespace;
selectNamespace(namespace);
showMenu.value = true;
setTimeout(() => {
document.addEventListener('click', hideContextMenu);
}, 0);
};
const isSelected = (namespace: KubernetesNamespace): boolean => {
if (!selectedNamespace.value) return false;
return selectedNamespace.value.metadata.name === namespace.metadata.name &&
selectedNamespace.value.metadata.uid === namespace.metadata.uid;
};
const selectNamespace = (namespace: KubernetesNamespace): void => {
const namespaceToEmit = { ...namespace };
if (!namespaceToEmit.kind) {
namespaceToEmit.kind = 'Namespace';
}
selectedNamespace.value = namespaceToEmit;
emit('namespace-selected', namespaceToEmit);
};
const deleteNamespace = (): void => {
if (selectedContextNamespace.value && props.selectedCluster) {
emit('delete-namespace', {
cluster: props.selectedCluster,
namespace: selectedNamespace.value
});
}
hideContextMenu();
};
const getUniqueKey = (namespace: KubernetesNamespace): string => {
return `${namespace.metadata.name}-${namespace.metadata.uid || ''}`;
};
const getNamespaceStatus = (namespace: KubernetesNamespace): string => {
return namespace.status?.phase || 'Unknown';
};
const getNamespaceStatusClass = (namespace: KubernetesNamespace): string => {
const status = getNamespaceStatus(namespace);
switch (status) {
case 'Active':
return 'status-active';
case 'Terminating':
return 'status-terminating';
default:
return 'status-unknown';
}
};
const editNamespace = (): void => {
showEditYaml.value = true;
hideContextMenu();
};
const createNamespace = (): void => {
showMethodSelector.value = true;
hideContextMenu();
};
const handleMethodSelected = (method: string): void => {
showMethodSelector.value = false;
if (method === 'guided') {
showCreateGuided.value = true;
} else if (method === 'yaml') {
showCreateYaml.value = true;
}
};
const handleUpdateNamespace = (): void => {
if (!props.selectedCluster) {
return
}
emit('load-resources')
}
const handleCreateNamespace = ({definition, yamlContent, isYaml}: { definition?: any, yamlContent?: string, isYaml?: boolean}): void => {
if (!props.selectedCluster) {
return
}
try {
let opts = {
cluster: props.selectedCluster,
isYaml: isYaml,
}
if (yamlContent) {
opts.yamlContent = yamlContent
emit('create-namespace', opts)
} else if (definition) {
opts.definition = definition
emit('create-namespace', opts)
} else {
throw new Error("Unable to emit create-namespace")
}
} catch (err: any) {
console.error('Failed to create namespace:', err)
} finally {
showCreateGuided.value = false
showCreateYaml.value = false
}
}
// Lifecycle hooks
onBeforeUnmount(() => {
document.removeEventListener('click', hideContextMenu);
});
// Return all refs, computed properties, and methods
return {
// State
showMethodSelector,
searchQuery,
selectedNamespace,
selectedContextNamespace,
showMenu,
showEditYaml,
menuPosition,
showCreateGuided,
showCreateYaml,
// Computed
filteredNamespaces,
// Methods
hideContextMenu,
showContextMenu,
isSelected,
selectNamespace,
deleteNamespace,
getUniqueKey,
getNamespaceStatus,
getNamespaceStatusClass,
formatAge,
handleMethodSelected,
createNamespace,
editNamespace,
handleCreateNamespace,
handleUpdateNamespace
};
}
});
</script>
<template>
<div class="namespaces-container">
<div class="search-bar-container">
<SearchBar
:value="searchQuery"
@update:value="searchQuery = $event"
placeholder="Search namespaces..."
/>
</div>
<div v-if="filteredNamespaces.length === 0" class="no-namespaces">
<p v-if="searchQuery">No namespaces found matching "{{ searchQuery }}"</p>
<p v-else>No namespaces found.</p>
</div>
<div v-else class="table-scroll-container">
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr
v-for="namespace in filteredNamespaces"
:key="getUniqueKey(namespace)"
:class="{ selected: isSelected(namespace) }"
@click="selectNamespace(namespace)"
@contextmenu="showContextMenu($event, namespace)"
>
<td>{{ namespace.metadata.name }}</td>
<td :class="getNamespaceStatusClass(namespace)">{{ getNamespaceStatus(namespace) }}</td>
<td>{{ formatAge(namespace.metadata.creationTimestamp) }}</td>
</tr>
</tbody>
</table>
<div
v-if="showMenu"
class="context-menu"
:style="{ top: menuPosition.y + 'px', left: menuPosition.x + 'px' }"
@click.stop
>
<div class="menu-item" @click="createNamespace">
Create
</div>
<div class="menu-item" @click="editNamespace">
Edit
</div>
<div class="menu-item" @click="deleteNamespace">
Delete
</div>
</div>
</div>
<NamespacesCreateMethodSelector
:show="showMethodSelector"
@close="showMethodSelector = false"
@method-selected="handleMethodSelected"
/>
<NamespacesCreateGuided
:show="showCreateGuided"
@close="showCreateGuided = false"
@create-namespace="handleCreateNamespace"
/>
<NamespacesCreateYaml
:show="showCreateYaml"
@close="showCreateYaml = false"
@create-namespace="handleCreateNamespace"
/>
<NamespacesEdit
:show="showEditYaml"
:namespace="selectedContextNamespace"
:cluster="selectedCluster"
@close="showEditYaml = false"
@namespace-updated="handleUpdateNamespace"
/>
</div>
</template>
<style src="@/assets/css/NamespacesList.css" scoped></style>