frontend/src/components/SecretsList.vue
<!-- SecretsList.vue -->
<script lang="ts">
import { defineComponent, ref, computed, PropType, onBeforeUnmount } from 'vue';
import { KubernetesCluster, KubernetesSecret } from '../types/kubernetes';
import { formatAge } from '../lib/format';
import SearchBar from './SearchBar.vue';
import SecretsCreateMethodSelector from './SecretsCreateMethodSelector.vue';
import SecretsCreateYaml from './SecretsCreateYaml.vue';
import SecretsCreateGuided from './SecretsCreateGuided.vue';
import SecretsEdit from './SecretsEdit.vue';
import { SecretUpdateOptions } from '../types/custom';
export default defineComponent({
name: 'SecretsList',
components: {
SearchBar,
SecretsCreateMethodSelector,
SecretsCreateYaml,
SecretsCreateGuided,
SecretsEdit
},
props: {
selectedCluster: {
type: Object as PropType<KubernetesCluster>,
required: false,
default: null
},
secrets: {
type: Array as PropType<KubernetesSecret[]>,
required: false,
default: () => []
}
},
emits: ['secret-selected', 'delete-secret', 'create-secret', 'update-secret'],
setup(props, { emit }) {
// State
const searchQuery = ref('');
const selectedSecret = ref<KubernetesSecret | null>(null);
const selectedContextSecret = ref<KubernetesSecret | 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 filteredSecrets = computed(() => {
if (!searchQuery.value) {
return props.secrets;
}
const query = searchQuery.value.toLowerCase();
const nameSpecificMatch = query.match(/^name:(.+)$/);
if (nameSpecificMatch) {
const nameQuery = nameSpecificMatch[1].trim();
return props.secrets.filter(secret => {
const name = secret.metadata.name.toLowerCase();
return name.includes(nameQuery);
});
}
const statusSpecificMatch = query.match(/^status:(.+)$/);
if (statusSpecificMatch) {
const statusQuery = statusSpecificMatch[1].trim();
return props.secrets.filter(secret => {
const status = secret.status?.phase?.toLowerCase() || '';
return status.includes(statusQuery);
});
}
const labelSpecificMatch = query.match(/^label:(.+)$/);
if (labelSpecificMatch) {
const labelQuery = labelSpecificMatch[1].trim();
return props.secrets.filter(secret => {
const labels = secret.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 secret names
return props.secrets.filter(secret => {
const name = secret.metadata.name.toLowerCase();
return name.includes(query);
});
});
// Methods
const hideContextMenu = () => {
showMenu.value = false;
document.removeEventListener('click', hideContextMenu);
};
const showContextMenu = (event: MouseEvent, secret: KubernetesSecret) => {
event.preventDefault();
menuPosition.value = {
x: event.clientX,
y: event.clientY
};
selectedContextSecret.value = secret;
selectSecret(secret);
showMenu.value = true;
setTimeout(() => {
document.addEventListener('click', hideContextMenu);
}, 0);
};
const isSelected = (secret: KubernetesSecret): boolean => {
if (!selectedSecret.value) return false;
return selectedSecret.value.metadata.name === secret.metadata.name &&
selectedSecret.value.metadata.uid === secret.metadata.uid;
};
const selectSecret = (secret: KubernetesSecret): void => {
try {
const secretToEmit = { ...secret };
if (!secretToEmit.kind) {
secretToEmit.kind = 'Secret';
}
selectedSecret.value = secretToEmit;
emit('secret-selected', secretToEmit);
} catch (error) {
console.error("Failed to select secret:", error);
}
};
const deleteSecret = (): void => {
if (selectedContextSecret.value && props.selectedCluster) {
try {
emit('delete-secret', {
cluster: props.selectedCluster,
secret: selectedContextSecret.value
});
} catch (error) {
alert(error);
}
}
hideContextMenu();
};
const getUniqueKey = (secret: KubernetesSecret): string => {
return `${secret.metadata.name}-${secret.metadata.uid || ''}`;
};
const getSecretStatus = (secret: KubernetesSecret): string => {
return secret.status?.phase || 'Unknown';
};
const getSecretStatusClass = (secret: KubernetesSecret): string => {
const status = getSecretStatus(secret);
switch (status) {
case 'Active':
return 'status-active';
case 'Terminating':
return 'status-terminating';
default:
return 'status-unknown';
}
};
const editSecret = (): void => {
showEditYaml.value = true;
hideContextMenu();
};
const createSecret = (): 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 handleUpdateSecret = (opts: SecretUpdateOptions): void => {
try {
emit('update-secret', opts)
} catch (error: any) {
alert(error)
}
return
}
const handleCreateSecret = (opts: SecretCreateOptions): void => {
try {
emit('create-secret', opts)
} catch (err: any) {
console.error('Failed to create Secret:', 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,
selectedSecret,
selectedContextSecret,
showMenu,
showEditYaml,
menuPosition,
showCreateGuided,
showCreateYaml,
// Computed
filteredSecrets,
// Methods
hideContextMenu,
showContextMenu,
isSelected,
selectSecret,
deleteSecret,
getUniqueKey,
getSecretStatus,
getSecretStatusClass,
formatAge,
handleMethodSelected,
createSecret,
editSecret,
handleCreateSecret,
handleUpdateSecret
};
}
});
</script>
<template>
<div class="secrets-container">
<div class="search-bar-container">
<SearchBar
:value="searchQuery"
@update:value="searchQuery = $event"
placeholder="Search secrets..."
/>
</div>
<div v-if="filteredSecrets.length === 0" class="no-secrets">
<p v-if="searchQuery">No secrets found matching "{{ searchQuery }}"</p>
<p v-else>No secrets found.</p>
</div>
<div v-else class="table-scroll-container">
<table>
<thead>
<tr>
<th>Namespace</th>
<th>Name</th>
<th>Type</th>
<th>Data</th>
<th>Age</th>
</tr>
</thead>
<tbody>
<tr
v-for="secret in filteredSecrets"
:key="getUniqueKey(secret)"
:class="{ selected: isSelected(secret) }"
@click="selectSecret(secret)"
@contextmenu="showContextMenu($event, secret)"
>
<td>{{ secret.metadata.namespace }}</td>
<td>{{ secret.metadata.name }}</td>
<td>{{ secret.type }}</td>
<td>{{ secret.data ? Object.keys(secret.data).length : 0 }}</td>
<td>{{ formatAge(secret.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="createSecret">
Create
</div>
<div class="menu-item" @click="editSecret">
Edit
</div>
<div class="menu-item" @click="deleteSecret">
Delete
</div>
</div>
</div>
<SecretsCreateMethodSelector
:show="showMethodSelector"
@close="showMethodSelector = false"
@method-selected="handleMethodSelected"
/>
<SecretsCreateYaml
:show="showCreateYaml"
@close="showCreateYaml = false"
@create-secret="handleCreateSecret"
/>
<SecretsCreateGuided
:show="showCreateGuided"
:cluster="selectedCluster"
@close="showCreateGuided = false"
@create-secret="handleCreateSecret"
/>
<SecretsEdit
:show="showEditYaml"
:secret="selectedContextSecret"
:cluster="selectedCluster"
@close="showEditYaml = false"
@update-secret="handleUpdateSecret"
/>
</div>
</template>
<style src="@/assets/css/ListResource.css" scoped></style>