Skip to content

Commit 5be7bf1

Browse files
committed
Add detail view for pipeline run
Signed-off-by: Keshav Priyadarshi <git@keshav.space>
1 parent 503dc5d commit 5be7bf1

File tree

7 files changed

+369
-1
lines changed

7 files changed

+369
-1
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Generated by Django 4.2.20 on 2025-05-01 12:53
2+
3+
import django.core.validators
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("vulnerabilities", "0091_alter_advisory_unique_together_and_more"),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name="PipelineSchedule",
17+
fields=[
18+
(
19+
"id",
20+
models.AutoField(
21+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
22+
),
23+
),
24+
(
25+
"pipeline_id",
26+
models.CharField(
27+
help_text="Identify a registered Pipeline class.",
28+
max_length=600,
29+
unique=True,
30+
),
31+
),
32+
(
33+
"is_active",
34+
models.BooleanField(
35+
db_index=True,
36+
default=True,
37+
help_text="When set to True (Yes), this Pipeline is active. When set to False (No), this Pipeline is inactive and not run.",
38+
null=True,
39+
),
40+
),
41+
(
42+
"run_interval",
43+
models.PositiveSmallIntegerField(
44+
default=1,
45+
help_text="Number of days to wait between run of this pipeline.",
46+
validators=[
47+
django.core.validators.MinValueValidator(
48+
1, message="Interval must be at least 1 day."
49+
),
50+
django.core.validators.MaxValueValidator(
51+
365, message="Interval must be at most 365 days."
52+
),
53+
],
54+
),
55+
),
56+
(
57+
"schedule_work_id",
58+
models.CharField(
59+
blank=True,
60+
db_index=True,
61+
help_text="Identifier used to manage the periodic run job.",
62+
max_length=255,
63+
null=True,
64+
unique=True,
65+
),
66+
),
67+
("created_date", models.DateTimeField(auto_now_add=True, db_index=True)),
68+
],
69+
options={
70+
"ordering": ["-created_date"],
71+
},
72+
),
73+
migrations.CreateModel(
74+
name="PipelineRun",
75+
fields=[
76+
(
77+
"id",
78+
models.AutoField(
79+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
80+
),
81+
),
82+
("run_id", models.CharField(blank=True, editable=False, null=True)),
83+
("run_start_date", models.DateTimeField(blank=True, editable=False, null=True)),
84+
("run_end_date", models.DateTimeField(blank=True, editable=False, null=True)),
85+
("run_exitcode", models.IntegerField(blank=True, editable=False, null=True)),
86+
("run_output", models.TextField(blank=True, editable=False)),
87+
("created_date", models.DateTimeField(auto_now_add=True, db_index=True)),
88+
("vulnerablecode_version", models.CharField(blank=True, max_length=100, null=True)),
89+
("vulnerablecode_commit", models.CharField(blank=True, max_length=300, null=True)),
90+
("log", models.TextField(blank=True, editable=False)),
91+
(
92+
"pipeline",
93+
models.ForeignKey(
94+
on_delete=django.db.models.deletion.CASCADE,
95+
related_name="pipelineruns",
96+
to="vulnerabilities.pipelineschedule",
97+
),
98+
),
99+
],
100+
options={
101+
"ordering": ["-created_date"],
102+
},
103+
),
104+
]
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
{% extends "base.html" %}
2+
{% load static %}
3+
{% load utils %}
4+
5+
{% block title %}Run Log{% endblock %}
6+
7+
{% block extrahead %}
8+
<style>
9+
pre {
10+
background-color: #282a36;
11+
color: #f8f8f2;
12+
padding: 1rem;
13+
border-radius: 4px;
14+
overflow-x: auto;
15+
max-height: 500px;
16+
}
17+
18+
.column {
19+
word-break: break-word;
20+
}
21+
</style>
22+
23+
<style>
24+
pre {
25+
background-color: #282a36;
26+
color: #f8f8f2;
27+
padding: 1.5rem 1rem 1rem 1rem;
28+
border-radius: 4px;
29+
overflow-x: auto;
30+
max-height: 500px;
31+
position: relative;
32+
}
33+
34+
.copy-btn {
35+
position: absolute;
36+
top: 1.0rem;
37+
right: 1.5rem;
38+
z-index: 1;
39+
opacity: 0.5;
40+
transition: opacity 0.2s ease;
41+
}
42+
</style>
43+
44+
{% endblock %}
45+
46+
{% block content %}
47+
<section class="section">
48+
<div class="container">
49+
<h1 class="title">{{ pipeline_name }} Run Log</h1>
50+
<hr>
51+
52+
<div class="box p-4">
53+
<div class="columns is-multiline is-vcentered is-mobile is-gapless">
54+
<div class="column is-one-fifth-desktop is-one-quarter-tablet is-half-mobile">
55+
<p class="is-size-7 has-text-weight-semibold">Pipeline ID</p>
56+
<p class="has-text-grey is-size-7 pr-3">{{ run.pipeline.pipeline_id }}</p>
57+
</div>
58+
<div class="column is-one-fifth-desktop is-one-quarter-tablet is-half-mobile">
59+
<p class="is-size-7 has-text-weight-semibold">Status</p>
60+
<p class="has-text-grey is-size-7">
61+
{% if run.status == "running" %}
62+
<i class="fa fa-spinner fa-spin has-text-info mr-1"></i>Running
63+
{% elif run.status == "success" %}
64+
<i class="fa fa-check-circle has-text-success mr-1"></i>Success
65+
{% elif run.status == "failure" %}
66+
<i class="fa fa-times-circle has-text-danger mr-1"></i>Failure
67+
{% elif run.status == "scheduled" %}
68+
<i class="fa fa-clock-o has-text-success mr-1"></i>Scheduled
69+
{% else %}
70+
<i class="fa fa-question-circle has-text-warning mr-1"></i>Unknown
71+
{% endif %}
72+
</p>
73+
</div>
74+
<div class="column is-one-fifth-desktop is-one-quarter-tablet is-half-mobile">
75+
<p class="is-size-7 has-text-weight-semibold">Execution Time</p>
76+
<p class="has-text-grey is-size-7">{{ run.execution_time }}</p>
77+
</div>
78+
<div class="column is-one-fifth-desktop is-one-quarter-tablet is-half-mobile">
79+
<p class="is-size-7 has-text-weight-semibold">Exit Code</p>
80+
<p class="has-text-grey is-size-7">{{ run.run_exitcode }}</p>
81+
</div>
82+
<div class="column is-one-fifth-desktop is-one-quarter-tablet is-half-mobile">
83+
<p class="is-size-7 has-text-weight-semibold">Start</p>
84+
<p class="has-text-grey is-size-7">{{ run.run_start_date }}</p>
85+
</div>
86+
<div class="column is-one-fifth-desktop is-one-quarter-tablet is-half-mobile ">
87+
<p class="is-size-7 has-text-weight-semibold">End</p>
88+
<p class="has-text-grey is-size-7">{{ run.run_end_date }}</p>
89+
</div>
90+
<div class="column is-one-fifth-desktop is-one-quarter-tablet is-half-mobile">
91+
<p class="is-size-7 has-text-weight-semibold">Created</p>
92+
<p class="has-text-grey is-size-7">{{ run.created_date }}</p>
93+
</div>
94+
<div class="column is-one-fifth-desktop is-one-quarter-tablet is-half-mobile">
95+
<p class="is-size-7 has-text-weight-semibold">Version</p>
96+
<p class="has-text-grey is-size-7">{{ run.vulnerablecode_version }}</p>
97+
</div>
98+
<div class="column is-one-fifth-desktop is-one-quarter-tablet is-half-mobile">
99+
<p class="is-size-7 has-text-weight-semibold">Commit</p>
100+
<p class="has-text-grey is-size-7">
101+
{% if run.vulnerablecode_commit %}
102+
<a href="https://github.com/aboutcode-org/vulnerablecode/tree/{{ run.vulnerablecode_commit }}"
103+
target="_blank">
104+
{{ run.vulnerablecode_commit }}
105+
<i class="fa fa-external-link fa_link_custom"></i>
106+
</a>
107+
{% endif %}
108+
109+
</p>
110+
</div>
111+
<div class="column is-one-fifth-desktop is-one-quarter-tablet is-half-mobile">
112+
<p class="is-size-7 has-text-weight-semibold">Job ID</p>
113+
<p class="has-text-grey is-size-7">{{ run.run_id }}</p>
114+
</div>
115+
</div>
116+
</div>
117+
118+
119+
{% if run.run_output|strip %}
120+
<div class="box">
121+
<h2 class="subtitle mb-2">Run Error</h2>
122+
<div class="log-wrapper" style="position: relative;">
123+
<button class="button is-medium is-light copy-btn" id="copy-error"
124+
onclick="copyCode('log-error', 'copy-error')">
125+
<span class="icon is-medium">
126+
<i class="fa fa-copy"></i>
127+
</span>
128+
</button>
129+
<pre><code id="log-error" class="language-toml">{{ run.run_output }}</code></pre>
130+
</div>
131+
</div>
132+
{% endif %}
133+
134+
{% if run.log|strip %}
135+
<div class="box">
136+
<h2 class="subtitle mb-2">Log Output</h2>
137+
<div class="log-wrapper" style="position: relative;">
138+
<button class="button is-medium is-light copy-btn" id="copy-code"
139+
onclick="copyCode('log-code', 'copy-code')">
140+
<span class="icon is-medium">
141+
<i class="fa fa-copy"></i>
142+
</span>
143+
</button>
144+
<pre><code id="log-code" class="language-toml">{{ run.log }}</code></pre>
145+
</div>
146+
</div>
147+
{% endif %}
148+
149+
150+
<a href="{% url 'runs-list' pipeline_id=run.pipeline.pipeline_id %}" class="button is-link mt-4">← Back to All
151+
Runs</a>
152+
</div>
153+
</section>
154+
{% endblock %}
155+
156+
157+
{% block scripts %}
158+
<link rel="stylesheet" href="{% static 'css/highlight-10.6.0.css' %}" crossorigin="anonymous">
159+
<script src="{% static 'js/highlight-10.6.0.min.js' %}" crossorigin="anonymous"></script>
160+
<script>hljs.highlightAll();</script>
161+
162+
<script>
163+
function copyCode(target, button) {
164+
const code = document.getElementById(target).innerText;
165+
navigator.clipboard.writeText(code)
166+
.then(() => {
167+
const btn = document.getElementById(button);
168+
btn.classList.add("is-success");
169+
btn.innerHTML = '<span class="icon is-small"><i class="fa fa-check"></i></span>';
170+
setTimeout(() => {
171+
btn.classList.remove("is-success");
172+
btn.innerHTML = '<span class="icon is-small"><i class="fa fa-copy"></i></span>';
173+
}, 1500);
174+
})
175+
.catch(err => alert("Copy failed: " + err));
176+
}
177+
</script>
178+
{% endblock %}

vulnerabilities/templatetags/show_cvss.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
111
from django import template
212
from django.utils.safestring import mark_safe
313

vulnerabilities/templatetags/url_filters.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
111
from urllib.parse import quote
212

3-
import packageurl
413
from django import template
514

615
register = template.Library()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
11+
from django import template
12+
13+
register = template.Library()
14+
15+
16+
@register.filter
17+
def strip(value):
18+
if isinstance(value, str):
19+
return value.strip()
20+
return value

vulnerabilities/views.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,3 +390,30 @@ def get_context_data(self, **kwargs):
390390
)
391391
context["pipeline_name"] = pipeline.pipeline_class.__name__
392392
return context
393+
394+
395+
class PipelineRunDetailView(DetailView):
396+
model = PipelineRun
397+
template_name = "pipeline_run_details.html"
398+
context_object_name = "run"
399+
400+
def get_object(self):
401+
pipeline_id = self.kwargs["pipeline_id"]
402+
run_id = self.kwargs["run_id"]
403+
return get_object_or_404(
404+
PipelineRun,
405+
pipeline__pipeline_id=pipeline_id,
406+
run_id=run_id,
407+
)
408+
409+
def get_context_data(self, **kwargs):
410+
context = super().get_context_data(**kwargs)
411+
pipeline_id = self.kwargs["pipeline_id"]
412+
run_id = self.kwargs["run_id"]
413+
run = get_object_or_404(
414+
PipelineRun,
415+
pipeline__pipeline_id=pipeline_id,
416+
run_id=run_id,
417+
)
418+
context["pipeline_name"] = run.pipeline_class.__name__
419+
return context

0 commit comments

Comments
 (0)