dn-scribe commited on
Commit
1bb6175
·
1 Parent(s): 0c35761

feat: products page

Browse files
Files changed (5) hide show
  1. app.py +2 -0
  2. models.py +1 -0
  3. templates/base.html +7 -1
  4. templates/products.html +76 -0
  5. web/products.py +55 -0
app.py CHANGED
@@ -59,6 +59,7 @@ with app.app_context():
59
  from api.login import login_bp
60
  from api.syslog import syslog_bp
61
  from web.routes import web_bp
 
62
  from api.webhooks import webhooks_bp
63
  from web.events import events_bp
64
 
@@ -67,4 +68,5 @@ with app.app_context():
67
  app.register_blueprint(syslog_bp, url_prefix='/')
68
  app.register_blueprint(webhooks_bp, url_prefix='/api')
69
  app.register_blueprint(web_bp)
 
70
  app.register_blueprint(events_bp)
 
59
  from api.login import login_bp
60
  from api.syslog import syslog_bp
61
  from web.routes import web_bp
62
+ from web.products import bp as products_bp
63
  from api.webhooks import webhooks_bp
64
  from web.events import events_bp
65
 
 
68
  app.register_blueprint(syslog_bp, url_prefix='/')
69
  app.register_blueprint(webhooks_bp, url_prefix='/api')
70
  app.register_blueprint(web_bp)
71
+ app.register_blueprint(products_bp)
72
  app.register_blueprint(events_bp)
models.py CHANGED
@@ -1,4 +1,5 @@
1
  from app import db
 
2
  from datetime import datetime, timedelta
3
  import secrets
4
  from sqlalchemy import JSON, Text, Boolean, Integer, String, DateTime, ForeignKey
 
1
  from app import db
2
+ from sqlalchemy import func
3
  from datetime import datetime, timedelta
4
  import secrets
5
  from sqlalchemy import JSON, Text, Boolean, Integer, String, DateTime, ForeignKey
templates/base.html CHANGED
@@ -41,7 +41,13 @@
41
  Dashboard
42
  </a>
43
  </li>
44
- <li class="nav-item">
 
 
 
 
 
 
45
  <a class="nav-link" href="{{ url_for('web.policy_results') }}">
46
  <i data-feather="shield" class="me-1"></i>
47
  Policy
 
41
  Dashboard
42
  </a>
43
  </li>
44
+ <li class="nav-item">
45
+ <a class="nav-link" href="{{ url_for('products.products') }}">
46
+ <i data-feather="package" class="me-1"></i>
47
+ Products
48
+ </a>
49
+ </li>
50
+ <li class="nav-item">
51
  <a class="nav-link" href="{{ url_for('web.policy_results') }}">
52
  <i data-feather="shield" class="me-1"></i>
53
  Policy
templates/products.html ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Products - Attestation Store{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container my-4">
7
+ <div class="row mb-4">
8
+ <div class="col">
9
+ <h1 class="h2">
10
+ <i data-feather="package" class="me-2"></i>
11
+ Products
12
+ </h1>
13
+ <p class="text-muted">Browse and filter products, versions, attestations, and policy results</p>
14
+ </div>
15
+ </div>
16
+
17
+ <!-- Search and Filters -->
18
+ <div class="card mb-4">
19
+ <div class="card-body">
20
+ <form method="GET" class="row g-3">
21
+ <div class="col-md-4">
22
+ <input type="text" class="form-control" name="filter" placeholder="Search products..." value="{{ filter_query }}">
23
+ </div>
24
+ <div class="col-md-2">
25
+ <button type="submit" class="btn btn-primary w-100">
26
+ <i data-feather="filter" class="me-1"></i> Filter
27
+ </button>
28
+ </div>
29
+ </form>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="card">
34
+ <div class="card-body p-0">
35
+ <div class="table-responsive">
36
+ <table class="table table-hover align-middle mb-0">
37
+ <thead class="table-dark">
38
+ <tr>
39
+ <th>Product</th>
40
+ <th>Version</th>
41
+ <th class="text-center">#Attestations</th>
42
+ <th class="text-center">Policy Results</th>
43
+ <th class="text-center">Actions</th>
44
+ </tr>
45
+ </thead>
46
+ <tbody>
47
+ {% for p in products %}
48
+ <tr>
49
+ <td>{{ p.product }}</td>
50
+ <td>{{ p.version }}</td>
51
+ <td class="text-center fw-bold">{{ p.attestations }}</td>
52
+ <td class="text-center">
53
+ {% if p.pass_count > 0 %}
54
+ <span class="badge bg-success">Pass: {{ p.pass_count }}</span>
55
+ {% endif %}
56
+ {% if p.non_pass_count > 0 %}
57
+ <span class="badge bg-danger ms-2">Non-pass: {{ p.non_pass_count }}</span>
58
+ {% endif %}
59
+ </td>
60
+ <td class="text-center">
61
+ <a href="/products/{{ p.product }}/attestations" class="btn btn-sm btn-outline-primary me-1">
62
+ <i data-feather="file-text" class="me-1"></i> Attestations
63
+ </a>
64
+ <a href="/products/{{ p.product }}/policy_results" class="btn btn-sm btn-outline-info">
65
+ <i data-feather="shield" class="me-1"></i> Policy Results
66
+ </a>
67
+ </td>
68
+ </tr>
69
+ {% endfor %}
70
+ </tbody>
71
+ </table>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ {% endblock %}
web/products.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, request, redirect, url_for
2
+ from models import db, Attestation, PolicyResult
3
+ from sqlalchemy import func
4
+ def get_products_with_stats(filter_query=None):
5
+ # Aggregate products and versions
6
+ query = db.session.query(
7
+ Attestation.product_key,
8
+ Attestation.product_version,
9
+ func.count(Attestation.id).label('attestations'),
10
+ ).filter(Attestation.deleted == False)
11
+ if filter_query:
12
+ query = query.filter(Attestation.product_key.ilike(f'%{filter_query}%'))
13
+ query = query.group_by(Attestation.product_key, Attestation.product_version)
14
+ products = []
15
+ for product_key, product_version, attestations_count in query:
16
+ # Policy results aggregation
17
+ pass_count = db.session.query(func.count(PolicyResult.id)).filter(
18
+ PolicyResult.product_key == product_key,
19
+ PolicyResult.product_version == product_version,
20
+ PolicyResult.result_kind == 'pass',
21
+ PolicyResult.deleted == False
22
+ ).scalar()
23
+ non_pass_count = db.session.query(func.count(PolicyResult.id)).filter(
24
+ PolicyResult.product_key == product_key,
25
+ PolicyResult.product_version == product_version,
26
+ PolicyResult.result_kind != 'pass',
27
+ PolicyResult.deleted == False
28
+ ).scalar()
29
+ products.append({
30
+ 'product': product_key,
31
+ 'version': product_version,
32
+ 'attestations': attestations_count,
33
+ 'pass_count': pass_count,
34
+ 'non_pass_count': non_pass_count
35
+ })
36
+ return products
37
+
38
+ bp = Blueprint('products', __name__)
39
+
40
+ @bp.route('/products')
41
+ def products():
42
+ # Get filter parameters from request
43
+ filter_query = request.args.get('filter', '')
44
+ products = get_products_with_stats(filter_query)
45
+ return render_template('products.html', products=products, filter_query=filter_query)
46
+
47
+ @bp.route('/products/<product>/attestations')
48
+ def product_attestations(product):
49
+ # Redirect to filtered attestations page
50
+ return redirect(url_for('web.attestations', product=product))
51
+
52
+ @bp.route('/products/<product>/policy_results')
53
+ def product_policy_results(product):
54
+ # Redirect to filtered policy results page
55
+ return redirect(url_for('web.policy_results', product=product))