Newer
Older
gnexus-creds / frontend / src / components / SecretDetailPanel.vue
<script setup>
import { computed } from "vue";
import {
  GnActionList,
  GnBadge,
  GnCopyButton,
  GnDescriptionList,
  GnDropdown,
  GnIconButton,
  GnPageHeader,
  GnToolbar,
  GnTooltip
} from "gnexus-ui-kit/vue";

import { t } from "../i18n";

const props = defineProps({
  secret: { type: Object, required: true },
  fields: { type: Array, default: null },
  showHeader: { type: Boolean, default: true },
  showBack: { type: Boolean, default: false },
  showActions: { type: Boolean, default: true },
  visibleFieldKeys: { type: Set, default: () => new Set() }
});

const emit = defineEmits([
  "back",
  "reveal",
  "hide",
  "edit-metadata",
  "toggle-field-visibility",
  "status-change",
  "archive",
  "delete",
  "navigate"
]);

const details = computed(() => [
  { label: t("detail.purpose"), value: props.secret.purpose || t("common.notSet") },
  { label: t("detail.category"), value: props.secret.category || t("common.notSet") },
  { label: t("detail.tags"), value: props.secret.tags?.join(", ") || t("detail.noTags") },
  {
    label: t("detail.access"),
    value: t("detail.accessState", {
      ui: props.secret.allow_ui ? t("detail.on") : t("detail.off"),
      rest: props.secret.allow_rest_api ? t("detail.on") : t("detail.off"),
      mcp: props.secret.allow_mcp ? t("detail.on") : t("detail.off")
    })
  }
]);

const fieldItems = computed(() =>
  (props.fields || []).map((field, index) => ({
    key: `${field.name}:${index}`,
    title: field.name,
    subtitle: field.encrypted ? t("detail.encrypted") : t("detail.plain"),
    raw: field
  }))
);

const actionItems = computed(() => [
  {
    label: t("detail.versions"),
    icon: "ph ph-clock-counter-clockwise",
    onSelect: () => emit("navigate", "history")
  },
  {
    label: t("nav.audit"),
    icon: "ph ph-list-checks",
    onSelect: () => emit("navigate", "audit")
  },
  {
    label: t("detail.editFields"),
    icon: "ph ph-list-plus",
    onSelect: () => emit("navigate", "edit-fields")
  },
  {
    label: props.secret.status === "actual" ? t("detail.markOutdated") : t("detail.markActual"),
    icon: "ph ph-arrows-clockwise",
    onSelect: () =>
      emit("status-change", props.secret.status === "actual" ? "outdated" : "actual")
  },
  { label: t("detail.archive"), icon: "ph ph-archive", onSelect: () => emit("archive") },
  {
    label: t("detail.deleteAction"),
    icon: "ph ph-trash",
    danger: true,
    onSelect: () => emit("delete")
  }
]);

function maskValue(value) {
  return "*".repeat(Math.max(8, String(value || "").length));
}

function isVisible(key) {
  return props.visibleFieldKeys.has(key);
}
</script>

<template>
  <GnPageHeader
    v-if="showHeader"
    :title="secret.title"
    compact
  >
    <template #kicker>
      <GnIconButton
        v-if="showBack"
        icon="ph ph-arrow-left"
        :label="t('detail.backToList')"
        @click="emit('back')"
      />
    </template>
    <template #actions>
      <div v-if="showActions" class="detail-actions">
        <GnBadge :variant="secret.status === 'actual' ? 'success' : 'warning'">
          {{ secret.status }}
        </GnBadge>
        <GnTooltip :text="fields ? t('detail.maskValues') : t('detail.showValues')">
          <GnIconButton
            :icon="fields ? 'ph ph-eye-slash' : 'ph ph-eye'"
            :label="fields ? t('detail.hideSecret') : t('detail.revealSecret')"
            @click="fields ? emit('hide') : emit('reveal')"
          />
        </GnTooltip>
        <GnTooltip :text="t('detail.editMetadata')">
          <GnIconButton
            icon="ph ph-pencil-simple"
            :label="t('detail.editMetadata')"
            @click="emit('edit-metadata')"
          />
        </GnTooltip>
        <GnDropdown :items="actionItems" class="secret-dropdown">
          <template #trigger="{ toggle }">
            <GnIconButton
              icon="ph ph-dots-three-outline"
              :label="t('detail.secretActions')"
              @click="toggle"
            />
          </template>
        </GnDropdown>
      </div>
    </template>
  </GnPageHeader>

  <GnDescriptionList :items="details" />

  <section v-if="fields?.length" class="workspace-surface inset-surface">
    <GnToolbar :title="t('detail.revealedFields')" />
    <GnActionList :items="fieldItems" class="revealed-fields-list">
      <template #controls="{ item }">
        <code class="secret-value">
          {{ isVisible(item.key) ? item.raw.value : maskValue(item.raw.value) }}
        </code>
        <GnTooltip :text="isVisible(item.key) ? t('detail.hideValue') : t('detail.showValue')">
          <GnIconButton
            :icon="isVisible(item.key) ? 'ph ph-eye-slash' : 'ph ph-eye'"
            :label="isVisible(item.key) ? t('detail.hideFieldValue') : t('detail.showFieldValue')"
            @click="emit('toggle-field-visibility', item.key)"
          />
        </GnTooltip>
        <GnCopyButton :text="item.raw.value" size="sm" />
      </template>
    </GnActionList>
  </section>
</template>