Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/main/frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { LoaderComponent } from './components/loader/loader.component';
import { FeedbackComponent } from './components/feedback/feedback.component';
import { HeaderComponent } from './pages/header/header.component';
import { TooltipComponent } from './pages/release-roadmap/issue-bar/tooltip/tooltip.component';
import { TooltipComponent } from './components/tooltip/tooltip.component';
import { AuthService } from './services/auth.service';
import { GraphStateService } from './services/graph-state.service';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<app-modal title="CVSS 3.1 Calculator" (closed)="close()">
<div class="cvss-calculator">
@if (referenceScore !== null) {
<p class="reference-note">
Original NVD score for reference: <strong>{{ referenceScore }}</strong>
</p>
}

<div class="vector-field">
<label for="cvssVectorInput">Paste a CVSS vector string (optional)</label>
<div class="vector-input-row">
<input
id="cvssVectorInput"
type="text"
class="vector-input"
placeholder="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
[(ngModel)]="vectorInput"
(keydown.enter)="applyVectorInput()"
/>
<button type="button" class="load-btn" (click)="applyVectorInput()">Load</button>
</div>
@if (vectorError()) {
<span class="error-message">{{ vectorError() }}</span>
}
</div>

<div class="metrics-grid">
@for (metric of metrics; track metric.key) {
<div class="metric-group">
<span
class="metric-label"
tabindex="0"
(mouseenter)="showTooltip($event, metric.label, metric.description)"
(focus)="showTooltip($event, metric.label, metric.description)"
(mouseleave)="hideTooltip()"
(blur)="hideTooltip()"
>
{{ metric.label }}
</span>
<div class="metric-options">
@for (option of metric.options; track option.key) {
<button
type="button"
class="metric-option"
[class.active]="selection()[metric.key] === option.key"
(click)="selectMetric(metric.key, option.key)"
(mouseenter)="showTooltip($event, option.fullLabel, option.description)"
(focus)="showTooltip($event, option.fullLabel, option.description)"
(mouseleave)="hideTooltip()"
(blur)="hideTooltip()"
>
{{ option.fullLabel }}
</button>
}
</div>
</div>
}
</div>

<div class="result-row">
<div class="result-score" [attr.data-severity]="result()?.severity ?? 'Incomplete'">
<span class="score-value">{{ result()?.score ?? '—' }}</span>
<span class="score-severity">{{ result()?.severity ?? 'Incomplete' }}</span>
</div>
@if (currentVectorString()) {
<code class="result-vector">{{ currentVectorString() }}</code>
}
</div>

<div class="actions-row">
<button type="button" class="cancel-btn" (click)="close()">Cancel</button>
<button type="button" class="use-btn" [disabled]="!result()" (click)="useScore()">Use this score</button>
</div>
</div>
</app-modal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
:host {
display: block;
}

.cvss-calculator {
display: flex;
flex-direction: column;
gap: 1.25rem;
overflow-x: hidden;
}

.reference-note {
margin: 0;
font-size: 0.875rem;
color: #6b7280;

strong {
color: #1f2937;
}
}

.vector-field {
display: flex;
flex-direction: column;
gap: 0.5rem;

label {
color: #1f2937;
font-weight: 600;
font-size: 0.85rem;
}
}

.vector-input-row {
display: flex;
gap: 0.5rem;
}

.vector-input {
flex: 1;
min-width: 0;
padding: 0.6rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 0.875rem;
background: #fff;
color: #1f2937;

&:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
}

.load-btn {
padding: 0.6rem 1rem;
background: #f3f4f6;
color: #1f2937;
border: 1px solid #d1d5db;
border-radius: 8px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;

&:hover {
background: #e5e7eb;
}
}

.error-message {
color: #dc2626;
font-weight: 600;
font-size: 0.8rem;
}

.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;

@media (max-width: 540px) {
grid-template-columns: 1fr;
}
}

.metric-group {
display: flex;
flex-direction: column;
gap: 0.4rem;
}

.metric-label {
font-weight: 600;
font-size: 0.8rem;
color: #6b7280;
cursor: help;
width: fit-content;
}

.metric-options {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}

.metric-option {
flex: 1;
min-width: 4.5rem;
padding: 0.4rem 0.35rem;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
color: #374151;
font-weight: 600;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.15s;

&:hover {
border-color: #93c5fd;
background: #eff6ff;
}

&.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
}

.result-row {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
padding-top: 0.5rem;
border-top: 1px solid #e5e7eb;
}

.result-score {
display: flex;
align-items: baseline;
gap: 0.4rem;
padding: 0.4rem 0.75rem;
border-radius: 8px;
background: #f3f4f6;
color: #374151;

.score-value {
font-weight: 700;
font-size: 1.2rem;
}

.score-severity {
font-weight: 600;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.02em;
}

&[data-severity='Critical'] {
background: #fee2e2;
color: #991b1b;
}

&[data-severity='High'] {
background: #fed7aa;
color: #9a3412;
}

&[data-severity='Medium'] {
background: #fef3c7;
color: #92400e;
}

&[data-severity='Low'] {
background: #dbeafe;
color: #1e40af;
}

&[data-severity='None'] {
background: #f3f4f6;
color: #6b7280;
}
}

.result-vector {
font-size: 0.8125rem;
color: #6b7280;
word-break: break-all;
}

.actions-row {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}

.cancel-btn {
padding: 0.65rem 1.25rem;
background: transparent;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 8px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;

&:hover {
background: #f3f4f6;
}
}

.use-btn {
padding: 0.65rem 1.25rem;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;

&:hover:not(:disabled) {
background: #2563eb;
}

&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
Loading
Loading