Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ad570d5
feat(ui): add Delete actions for catalog/group/control and fix button…
AKAbdulHanif Jan 15, 2026
c759ae2
ci: enable PR comments using pull_request_target with write permissions
AKAbdulHanif Jan 15, 2026
8f34c01
ci: remove pull_request_target; switch ESLint report to step summary
AKAbdulHanif Jan 16, 2026
35d1a95
ci(ui): resolve merge markers; remove pull_request_target; use ESLint…
AKAbdulHanif Jan 27, 2026
1763f71
fix(ui): correct template closing tags in CatalogGroup and CatalogCon…
AKAbdulHanif Jan 27, 2026
35fb639
feat(ui): add ProfileBuildPropsView for prop-based builder
AKAbdulHanif Jan 29, 2026
26a454d
feat(ui): add catalog selector and UUID validation in props builder
AKAbdulHanif Jan 30, 2026
687ea1e
feat(ui): add prominent Build Props button to Profiles header
AKAbdulHanif Jan 30, 2026
25852c2
fix(ui): rename route to profile-build-props and update links to reso…
AKAbdulHanif Jan 30, 2026
48e41dc
fix(ui): wire Add Statement button to open statement editor
AKAbdulHanif Feb 2, 2026
b8ddc57
chore(ui): default props rule to label contains AC- for positive matc…
AKAbdulHanif Feb 2, 2026
68b8ac4
fix(ui): block submit on empty rule set and trim values in builder
AKAbdulHanif Feb 2, 2026
622dedd
fix(ui): restore system-component-dashboards route for ComponentsView…
AKAbdulHanif Feb 2, 2026
9c7e198
feat(ui): remove Admin builder link and open builder as modal from Pr…
AKAbdulHanif Feb 3, 2026
4757b4b
fix: re-add routes and menus to features
gusfcarvalho Feb 3, 2026
c41fced
feat(poam): add basic POAM items list page and route
AKAbdulHanif Mar 4, 2026
261397e
docs(poam): add UI design document
AKAbdulHanif Mar 4, 2026
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
38 changes: 38 additions & 0 deletions docs/POAM-Design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Title: POAM Phase 1 – UI Design

Goals

- Provide a basic POAM Items list page to exercise the API filters end-to-end.
- Keep UX simple for Phase 1; extend with create/edit flows in Phase 2.

Routing

- New authenticated route: /poam-items
- Router entry added under app routes; guard via existing auth store.

Data Access

- Uses existing composables (useFetch/useApi) to call /api/poam-items
- Query params supported: status, sspId, riskId, deadlineBefore (RFC3339)

View: CCFPoamItemsListView

- Filters
- Dropdown: status (open|in-progress|completed|overdue)
- Inputs: sspId UUID, riskId UUID, deadlineBefore RFC3339
- Apply triggers a reload with current params
- Table Columns
- Title, Status, Deadline, POC (name/email), Updated

Future Enhancements

- Create/Edit forms for POAM items and milestones
- Mark milestone completed, auto-set completed_at
- Link POAM item to risks from the UI
- Pagination and sorting

Verification

- API is reachable at /api/poam-items
- UI config.json provides API_URL
- Manual test by applying filters and verifying table updates
21 changes: 21 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ const authenticatedRoutes = [
},
],
},
{
path: 'components/dashboards/:componentId',
name: 'system-component-dashboards',
component: () => import('../views/system/ComponentsView.vue'),
},
{
path: 'authorizations',
name: 'system:authorizations',
Expand All @@ -110,6 +115,14 @@ const authenticatedRoutes = [
requiresAuth: true,
},
},
{
path: 'poam-items',
name: 'poam-items:index',
component: () => import('../views/poam/CCFPoamItemsListView.vue'),
meta: {
requiresAuth: true,
},
},
{
path: 'risks/:riskId',
name: 'risks:detail',
Expand Down Expand Up @@ -194,6 +207,14 @@ const authenticatedRoutes = [
requiresAuth: true,
},
},
{
path: '/profiles/build-props',
name: 'profile-build-props',
component: () => import('../views/profile/ProfileBuildPropsView.vue'),
meta: {
requiresAuth: true,
},
},
{
path: '/profiles/:id',
name: 'profile:view',
Expand Down
5 changes: 0 additions & 5 deletions src/views/LeftSideNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,6 @@ const links = ref<Array<NavigationItem>>([
name: 'users-list',
title: 'System Users',
},
{
name: 'admin-import',
title: 'Import',
abbr: 'IMP',
},
],
},
]);
Expand Down
45 changes: 42 additions & 3 deletions src/views/catalog/CatalogControl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,25 @@
>
<div class="flex items-start justify-between gap-4">
<div>
<TertiaryButton v-if="!statement">Add Statement</TertiaryButton>
<TertiaryButton v-if="!objective" class="ml-2"
<TertiaryButton v-if="!statement" @click="showEditStatement = true"
>Add Statement</TertiaryButton
>
<TertiaryButton
v-if="statement"
class="ml-2"
@click="showEditStatement = true"
>Edit Statement</TertiaryButton
>
<TertiaryButton
v-if="!objective"
class="ml-2"
@click="showEditObjective = true"
>Add Objective</TertiaryButton
>
<TertiaryButton v-if="!guidance" class="ml-2"
<TertiaryButton
v-if="!guidance"
class="ml-2"
@click="showEditGuidance = true"
>Add Guidance</TertiaryButton
>

Expand Down Expand Up @@ -100,6 +114,27 @@
:parent-control="props.control"
v-model="showControlForm"
/>
<ControlPartEditModal
v-model="showEditStatement"
:catalog="catalog"
:control="props.control"
type="statement"
@updated="onUpdated"
/>
<ControlPartEditModal
v-model="showEditObjective"
:catalog="catalog"
:control="props.control"
type="assessment-objective"
@updated="onUpdated"
/>
<ControlPartEditModal
v-model="showEditGuidance"
:catalog="catalog"
:control="props.control"
type="guidance"
@updated="onUpdated"
/>
<ControlEditModal
v-model="showEdit"
:catalog="catalog"
Expand All @@ -118,6 +153,7 @@ import { type Catalog, type Control } from '@/oscal';
import TertiaryButton from '@/volt/TertiaryButton.vue';
import ControlCreateModal from '@/components/catalogs/ControlCreateModal.vue';
import ControlEditModal from '@/components/catalogs/ControlEditModal.vue';
import ControlPartEditModal from '@/components/catalogs/ControlPartEditModal.vue';
import type { Part } from '@/oscal';
import PartDisplayEditor from '@/components/PartDisplayEditor.vue';
import { useRouter } from 'vue-router';
Expand Down Expand Up @@ -179,6 +215,9 @@ function getPart(type: string) {

const showControlForm = ref<boolean>(false);
const showEdit = ref<boolean>(false);
const showEditStatement = ref<boolean>(false);
const showEditObjective = ref<boolean>(false);
const showEditGuidance = ref<boolean>(false);

function controlCreated(control: Control) {
controls.value?.push(control);
Expand Down
1 change: 0 additions & 1 deletion src/views/catalog/CatalogView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ async function deleteCatalog(uuid: string, title: string) {
function deleteCurrentCatalog() {
deleteCatalog(catalogId.value, catalog.value?.metadata?.title || '');
}

function reloadLists() {
groupExecute(`/api/oscal/catalogs/${catalogId.value}/groups`);
catalogExecute(`/api/oscal/catalogs/${catalogId.value}/controls`);
Expand Down
137 changes: 137 additions & 0 deletions src/views/poam/CCFPoamItemsListView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<template>
<div class="p-6">
<h1 class="text-2xl font-semibold mb-4">POAM Items</h1>
<div class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
<div>
<label class="block text-sm font-medium mb-1">Status</label>
<select v-model="status" class="border rounded px-2 py-1 w-full">
<option value="">Any</option>
<option value="open">open</option>
<option value="in-progress">in-progress</option>
<option value="completed">completed</option>
<option value="overdue">overdue</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">SSP ID</label>
<input
v-model="sspId"
placeholder="UUID"
class="border rounded px-2 py-1 w-full"
/>
</div>
<div>
<label class="block text-sm font-medium mb-1">Risk ID</label>
<input
v-model="riskId"
placeholder="UUID"
class="border rounded px-2 py-1 w-full"
/>
</div>
<div>
<label class="block text-sm font-medium mb-1">Deadline Before</label>
<input
v-model="deadlineBefore"
placeholder="YYYY-MM-DDTHH:mm:ssZ"
class="border rounded px-2 py-1 w-full"
/>
</div>
</div>
<div class="mb-4">
<button @click="refresh" class="bg-blue-600 text-white px-3 py-1 rounded">
Apply Filters
</button>
</div>
<div v-if="loading" class="text-gray-600">Loading...</div>
<div v-else-if="error" class="text-red-600">Failed to load POAM items</div>
<table v-else class="min-w-full border rounded">
<thead>
<tr class="bg-gray-100 text-left text-sm">
<th class="p-2 border">Title</th>
<th class="p-2 border">Status</th>
<th class="p-2 border">Deadline</th>
<th class="p-2 border">POC</th>
<th class="p-2 border">Updated</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id" class="text-sm">
<td class="p-2 border">{{ item.title }}</td>
<td class="p-2 border">{{ item.status }}</td>
<td class="p-2 border">{{ item.deadline ?? '-' }}</td>
<td class="p-2 border">
{{ item.pocName ?? '-' }}
<span v-if="item.pocEmail">({{ item.pocEmail }})</span>
</td>
<td class="p-2 border">{{ item.updatedAt }}</td>
</tr>
</tbody>
</table>
</div>
</template>

<script setup lang="ts">
import { ref, watchEffect, computed } from 'vue';
import { useApi } from '@/composables/api';

type PoamItem = {
id: string;
sspId: string;
title: string;
description: string;
status: string;
deadline?: string | null;
resourceRequired?: string | null;
pocName?: string | null;
pocEmail?: string | null;
pocPhone?: string | null;
remarks?: string | null;
createdAt: string;
updatedAt: string;
};

type ListResponse = { data: PoamItem[] };

const status = ref<string>('');
const sspId = ref<string>('');
const riskId = ref<string>('');
const deadlineBefore = ref<string>('');

const url = () => {
const params = new URLSearchParams();
if (status.value) params.set('status', status.value);
if (sspId.value) params.set('sspId', sspId.value);
if (riskId.value) params.set('riskId', riskId.value);
if (deadlineBefore.value) params.set('deadlineBefore', deadlineBefore.value);
const qs = params.toString();
return new Request(`/api/poam-items${qs ? `?${qs}` : ''}`, {
method: 'GET',
headers: new Headers(),
});
};

const data = ref<ListResponse>();
const loading = ref<boolean>(true);
const error = ref<boolean>(false);

function load() {
loading.value = true;
error.value = false;
const { data: d, loading: l, error: e } = useApi<ListResponse>(url());
watchEffect(() => {
data.value = d.value;
loading.value = l.value;
error.value = e.value;
});
}

function refresh() {
load();
}

load();

const items = computed(() => data.value?.data ?? []);
</script>

<style scoped></style>
Loading