refactor: html output custom style

main
sundowndev 2021-06-08 17:14:28 +02:00
parent a14a72f212
commit a27d993b13
7 changed files with 1129 additions and 71 deletions

View File

@ -4,16 +4,28 @@
<title>Driftctl scan report</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="shortcut icon"
href="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/favicon.ico"/>
<style>{{.Stylesheet}}</style>
</head>
<body>
<div class="container">
<div class="heading d-flex justify-space-between align-center">
<div>
<h1 class="heading-title">Driftctl scan report</h1>
<h1 class="heading-title mb-1">Driftctl scan report</h1>
<span class="heading-subtitle">Coverage {{.Coverage}}%</span>
</div>
<h2 class="heading-date">{{ .ScanDate }}</h2>
<div class="text--right">
<h2 class="heading-date mb-1">{{ .ScanDate }}</h2>
<span class="heading-subtitle">Scan duration {{.ScanDuration}}</span>
</div>
</div>
<div class="card d-flex justify-space-around mb-2 text--grey">
<span>Total resources: {{.Summary.TotalResources}}</span>
<span>Managed: {{rate .Summary.TotalManaged}}%</span>
<span>Changed: {{.Summary.TotalDrifted}}/{{.Summary.TotalManaged}}</span>
<span>Unmanaged: {{rate .Summary.TotalUnmanaged}}%</span>
<span>Missing: {{rate .Summary.TotalDeleted}}%</span>
</div>
<div class="app-content">
{{ if (lt .Coverage 100) }}
@ -56,16 +68,24 @@
<div class="panels">
{{ if (gt (len .Unmanaged) 0) }}
<div class="panel" id="one-panel">
<div class="d-flex justify-space-between text--grey pa-1">
<span>Resource id</span>
<span>Resource type</span>
</div>
{{range $res := .Unmanaged}}
<div class="resource-item resource-item-unmanaged d-flex justify-space-between">
<span class="resource-item-id">{{$res.TerraformId}}</span>
<span class="resource-item-type text--bold">{{$res.TerraformType}}</span>
<span class="resource-item-type">{{$res.TerraformType}}</span>
</div>
{{end}}
</div>
{{end}}
{{ if (gt (len .Differences) 0) }}
<div class="panel" id="two-panel">
<div class="d-flex justify-space-between text--grey pa-1">
<span>Resource id</span>
<span>Resource type</span>
</div>
{{range $diff := .Differences}}
<div class="resource-item resource-item-changed d-flex justify-space-between">
<span class="resource-item-id">{{$diff.Res.TerraformId}}</span>
@ -74,17 +94,21 @@
<div>{{ formatChange $change }}</div>
{{end}}
</div>
<span class="resource-item-type text--bold">{{$diff.Res.TerraformType}}</span>
<span class="resource-item-type">{{$diff.Res.TerraformType}}</span>
</div>
{{end}}
</div>
{{end}}
{{ if (gt (len .Deleted) 0) }}
<div class="panel" id="three-panel">
<div class="d-flex justify-space-between text--grey pa-1">
<span>Resource id</span>
<span>Resource type</span>
</div>
{{range $res := .Deleted}}
<div class="resource-item resource-item-deleted d-flex justify-space-between">
<span class="resource-item-id">{{$res.TerraformId}}</span>
<span class="resource-item-type text--bold">{{$res.TerraformType}}</span>
<span class="resource-item-type">{{$res.TerraformType}}</span>
</div>
{{end}}
</div>
@ -94,8 +118,8 @@
{{range $type, $messages := .Alerts}}
{{range $el := $messages}}
<div class="resource-item resource-item-alerts">
<span class="resource-item-type text--bold">{{ $type }}</span>
<span>- {{ $el.Message }}</span>
<span class="resource-item-type">{{ $type }}</span>
<span>{{ $el.Message }}</span>
</div>
{{end}}
{{end}}

View File

@ -1,5 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600&display=swap');
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
@ -20,7 +18,6 @@ time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
@ -45,7 +42,6 @@ blockquote, q {
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
@ -62,7 +58,7 @@ table {
}
body {
font-family: 'Source Sans Pro', sans-serif;
font-family: "Helvetica", sans-serif;
color: #1C1E21;
background-color: #F7F7F9;
padding-bottom: 50px;
@ -127,6 +123,26 @@ div.container {
margin-bottom: 10px;
}
.pa-1 {
padding: 10px;
}
.pl-1 {
padding-left: 10px;
}
.pr-1 {
padding-right: 10px;
}
.pt-1 {
padding-top: 10px;
}
.pb-1 {
padding-bottom: 10px;
}
input[type="text"],
select {
padding: 8px;
@ -144,6 +160,7 @@ select:focus {
input[name="resource-id-filter"], select[name="resource-type-filter"] {
width: 300px;
border-radius: 3px;
}
h1 {
@ -157,21 +174,32 @@ h2 {
}
.heading-title {
margin-bottom: 10px;
display: block;
}
.heading-subtitle {
font-size: 14px;
display: block;
}
.app-content {
padding: 25px;
border-top: 3px solid #71B2C3;
background-color: #ffffff;
border-radius: 0 0 10px 10px;
box-shadow: 0 0 5px #0000000a;
}
.resource-item {
border: 1px solid #ececec;
padding: 10px;
margin-top: 10px;
border-top: 1px solid #ececec;
border-left: 1px solid #ececec;
border-right: 1px solid #ececec;
padding: 15px;
color: #6e7071;
font-size: 14px;
}
.resource-item:last-child {
border-bottom: 1px solid #ececec;
}
.resource-item:hover {
@ -182,6 +210,22 @@ h2 {
font-weight: bold;
}
.text--grey {
color: #747578;
}
.text--right {
text-align: right;
}
.card {
padding: 15px;
font-size: 15px;
background: #ffffff;
box-shadow: 0 0 5px #0000000a;
border-radius: 3px;
}
.reset-filter-btn {
border: none;
padding: 8px;
@ -206,6 +250,7 @@ h2 {
display: inline-block;
color: #747578;
transition: background-color 200ms;
border-radius: 3px;
}
.tab:hover {
@ -251,9 +296,8 @@ h2 {
#two:checked ~ .tabs #two-tab,
#three:checked ~ .tabs #three-tab,
#four:checked ~ .tabs #four-tab {
background: transparent;
color: #747578;
border-bottom: 2px solid #71b2c3;
background: #71b2c3;
color: #ffffff;
}
.empty-message-container {

View File

@ -4,6 +4,7 @@ import (
"embed"
"fmt"
"html/template"
"math"
"os"
"strings"
"time"
@ -34,6 +35,7 @@ type HTMLTemplateParams struct {
Deleted []resource.Resource
Alerts alerter.Alerts
Stylesheet template.CSS
ScanDuration string
}
func NewHTML(path string) *HTML {
@ -92,6 +94,12 @@ func (c *HTML) Write(analysis *analyser.Analysis) error {
return fmt.Sprintf("%s %s: %s => %s %s", prefix, strings.Join(ch.Path, "."), prettify(ch.From), prettify(ch.To), suffix)
},
"rate": func(count int) float64 {
if analysis.Summary().TotalResources == 0 {
return 0
}
return math.Round(100 * float64(count) / float64(analysis.Summary().TotalResources))
},
}
tmpl, err := template.New("main").Funcs(funcMap).Parse(string(tmplFile))
@ -108,6 +116,7 @@ func (c *HTML) Write(analysis *analyser.Analysis) error {
Deleted: analysis.Deleted(),
Alerts: analysis.Alerts(),
Stylesheet: template.CSS(styleFile),
ScanDuration: analysis.Duration.Round(time.Second).String(),
}
err = tmpl.Execute(file, data)

View File

@ -4,6 +4,7 @@ import (
"io/ioutil"
"path"
"testing"
"time"
"github.com/cloudskiff/driftctl/pkg/resource"
testresource "github.com/cloudskiff/driftctl/test/resource"
@ -21,12 +22,38 @@ func TestHTML_Write(t *testing.T) {
analysis func() *analyser.Analysis
err error
}{
{
name: "test html output when there's no resources",
goldenfile: "output_empty.html",
analysis: func() *analyser.Analysis {
a := &analyser.Analysis{}
return a
},
err: nil,
},
{
name: "test html output when infrastructure is in sync",
goldenfile: "output_sync.html",
analysis: func() *analyser.Analysis {
a := &analyser.Analysis{}
a.Duration = 72 * time.Second
a.AddManaged(
&testresource.FakeResource{
Id: "deleted-id-3",
Type: "aws_deleted_resource",
},
)
return a
},
err: nil,
},
{
name: "test html output",
goldenfile: "output.html",
analysis: func() *analyser.Analysis {
a := fakeAnalysisWithAlerts()
a.Duration = 91 * time.Second
a.AddDeleted(
&testresource.FakeResource{
Id: "deleted-id-3",

View File

@ -4,9 +4,9 @@
<title>Driftctl scan report</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style>@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600&display=swap');
/* http://meyerweb.com/eric/tools/css/reset/
<link rel="shortcut icon"
href="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/favicon.ico"/>
<style>/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
@ -26,7 +26,6 @@ time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
@ -51,7 +50,6 @@ blockquote, q {
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
@ -68,7 +66,7 @@ table {
}
body {
font-family: 'Source Sans Pro', sans-serif;
font-family: "Helvetica", sans-serif;
color: #1C1E21;
background-color: #F7F7F9;
padding-bottom: 50px;
@ -133,6 +131,26 @@ div.container {
margin-bottom: 10px;
}
.pa-1 {
padding: 10px;
}
.pl-1 {
padding-left: 10px;
}
.pr-1 {
padding-right: 10px;
}
.pt-1 {
padding-top: 10px;
}
.pb-1 {
padding-bottom: 10px;
}
input[type="text"],
select {
padding: 8px;
@ -150,6 +168,7 @@ select:focus {
input[name="resource-id-filter"], select[name="resource-type-filter"] {
width: 300px;
border-radius: 3px;
}
h1 {
@ -163,21 +182,32 @@ h2 {
}
.heading-title {
margin-bottom: 10px;
display: block;
}
.heading-subtitle {
font-size: 14px;
display: block;
}
.app-content {
padding: 25px;
border-top: 3px solid #71B2C3;
background-color: #ffffff;
border-radius: 0 0 10px 10px;
box-shadow: 0 0 5px #0000000a;
}
.resource-item {
border: 1px solid #ececec;
padding: 10px;
margin-top: 10px;
border-top: 1px solid #ececec;
border-left: 1px solid #ececec;
border-right: 1px solid #ececec;
padding: 15px;
color: #6e7071;
font-size: 14px;
}
.resource-item:last-child {
border-bottom: 1px solid #ececec;
}
.resource-item:hover {
@ -188,6 +218,22 @@ h2 {
font-weight: bold;
}
.text--grey {
color: #747578;
}
.text--right {
text-align: right;
}
.card {
padding: 15px;
font-size: 15px;
background: #ffffff;
box-shadow: 0 0 5px #0000000a;
border-radius: 3px;
}
.reset-filter-btn {
border: none;
padding: 8px;
@ -212,6 +258,7 @@ h2 {
display: inline-block;
color: #747578;
transition: background-color 200ms;
border-radius: 3px;
}
.tab:hover {
@ -257,9 +304,8 @@ h2 {
#two:checked ~ .tabs #two-tab,
#three:checked ~ .tabs #three-tab,
#four:checked ~ .tabs #four-tab {
background: transparent;
color: #747578;
border-bottom: 2px solid #71b2c3;
background: #71b2c3;
color: #ffffff;
}
.empty-message-container {
@ -270,10 +316,20 @@ h2 {
<div class="container">
<div class="heading d-flex justify-space-between align-center">
<div>
<h1 class="heading-title">Driftctl scan report</h1>
<h1 class="heading-title mb-1">Driftctl scan report</h1>
<span class="heading-subtitle">Coverage 15%</span>
</div>
<h2 class="heading-date">Jun 07, 2021</h2>
<div class="text--right">
<h2 class="heading-date mb-1">Jun 08, 2021</h2>
<span class="heading-subtitle">Scan duration 1m31s</span>
</div>
</div>
<div class="card d-flex justify-space-around mb-2 text--grey">
<span>Total resources: 13</span>
<span>Managed: 15%</span>
<span>Changed: 2/2</span>
<span>Unmanaged: 38%</span>
<span>Missing: 46%</span>
</div>
<div class="app-content">
@ -320,36 +376,44 @@ h2 {
<div class="panels">
<div class="panel" id="one-panel">
<div class="d-flex justify-space-between text--grey pa-1">
<span>Resource id</span>
<span>Resource type</span>
</div>
<div class="resource-item resource-item-unmanaged d-flex justify-space-between">
<span class="resource-item-id">unmanaged-id-1</span>
<span class="resource-item-type text--bold">aws_unmanaged_resource</span>
<span class="resource-item-type">aws_unmanaged_resource</span>
</div>
<div class="resource-item resource-item-unmanaged d-flex justify-space-between">
<span class="resource-item-id">unmanaged-id-2</span>
<span class="resource-item-type text--bold">aws_unmanaged_resource</span>
<span class="resource-item-type">aws_unmanaged_resource</span>
</div>
<div class="resource-item resource-item-unmanaged d-flex justify-space-between">
<span class="resource-item-id">unmanaged-id-3</span>
<span class="resource-item-type text--bold">aws_unmanaged_resource</span>
<span class="resource-item-type">aws_unmanaged_resource</span>
</div>
<div class="resource-item resource-item-unmanaged d-flex justify-space-between">
<span class="resource-item-id">unmanaged-id-4</span>
<span class="resource-item-type text--bold">aws_unmanaged_resource</span>
<span class="resource-item-type">aws_unmanaged_resource</span>
</div>
<div class="resource-item resource-item-unmanaged d-flex justify-space-between">
<span class="resource-item-id">unmanaged-id-5</span>
<span class="resource-item-type text--bold">aws_unmanaged_resource</span>
<span class="resource-item-type">aws_unmanaged_resource</span>
</div>
</div>
<div class="panel" id="two-panel">
<div class="d-flex justify-space-between text--grey pa-1">
<span>Resource id</span>
<span>Resource type</span>
</div>
<div class="resource-item resource-item-changed d-flex justify-space-between">
<span class="resource-item-id">diff-id-1</span>
@ -362,7 +426,7 @@ h2 {
<div>- a: &#34;oldValue&#34; =&gt; &lt;nil&gt; </div>
</div>
<span class="resource-item-type text--bold">aws_diff_resource</span>
<span class="resource-item-type">aws_diff_resource</span>
</div>
<div class="resource-item resource-item-changed d-flex justify-space-between">
@ -372,42 +436,46 @@ h2 {
<div>- path.to.field: &lt;nil&gt; =&gt; [&#34;value&#34;] </div>
</div>
<span class="resource-item-type text--bold">aws_diff_resource</span>
<span class="resource-item-type">aws_diff_resource</span>
</div>
</div>
<div class="panel" id="three-panel">
<div class="d-flex justify-space-between text--grey pa-1">
<span>Resource id</span>
<span>Resource type</span>
</div>
<div class="resource-item resource-item-deleted d-flex justify-space-between">
<span class="resource-item-id">deleted-id-1</span>
<span class="resource-item-type text--bold">aws_deleted_resource</span>
<span class="resource-item-type">aws_deleted_resource</span>
</div>
<div class="resource-item resource-item-deleted d-flex justify-space-between">
<span class="resource-item-id">deleted-id-2</span>
<span class="resource-item-type text--bold">aws_deleted_resource</span>
<span class="resource-item-type">aws_deleted_resource</span>
</div>
<div class="resource-item resource-item-deleted d-flex justify-space-between">
<span class="resource-item-id">deleted-id-3</span>
<span class="resource-item-type text--bold">aws_deleted_resource</span>
<span class="resource-item-type">aws_deleted_resource</span>
</div>
<div class="resource-item resource-item-deleted d-flex justify-space-between">
<span class="resource-item-id">deleted-id-4</span>
<span class="resource-item-type text--bold">aws_deleted_resource</span>
<span class="resource-item-type">aws_deleted_resource</span>
</div>
<div class="resource-item resource-item-deleted d-flex justify-space-between">
<span class="resource-item-id">deleted-id-5</span>
<span class="resource-item-type text--bold">aws_deleted_resource</span>
<span class="resource-item-type">aws_deleted_resource</span>
</div>
<div class="resource-item resource-item-deleted d-flex justify-space-between">
<span class="resource-item-id">deleted-id-6</span>
<span class="resource-item-type text--bold">aws_deleted_resource</span>
<span class="resource-item-type">aws_deleted_resource</span>
</div>
</div>
@ -417,18 +485,18 @@ h2 {
<div class="resource-item resource-item-alerts">
<span class="resource-item-type text--bold"></span>
<span>- Ignoring aws_vpc from drift calculation: Listing aws_vpc is forbidden.</span>
<span class="resource-item-type"></span>
<span>Ignoring aws_vpc from drift calculation: Listing aws_vpc is forbidden.</span>
</div>
<div class="resource-item resource-item-alerts">
<span class="resource-item-type text--bold"></span>
<span>- Ignoring aws_sqs from drift calculation: Listing aws_sqs is forbidden.</span>
<span class="resource-item-type"></span>
<span>Ignoring aws_sqs from drift calculation: Listing aws_sqs is forbidden.</span>
</div>
<div class="resource-item resource-item-alerts">
<span class="resource-item-type text--bold"></span>
<span>- Ignoring aws_sns from drift calculation: Listing aws_sns is forbidden.</span>
<span class="resource-item-type"></span>
<span>Ignoring aws_sns from drift calculation: Listing aws_sns is forbidden.</span>
</div>

View File

@ -0,0 +1,458 @@
<!doctype html>
<html>
<head>
<title>Driftctl scan report</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="shortcut icon"
href="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/favicon.ico"/>
<style>/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/*
* Custom style
*/
:root {
--transitionDuration: 400ms;
}
body {
font-family: "Helvetica", sans-serif;
color: #1C1E21;
background-color: #F7F7F9;
padding-bottom: 50px;
}
div.container {
max-width: 100%;
width: 1280px;
margin: auto;
}
.heading {
height: 130px;
}
.hide {
display: none !important;
}
.d-flex {
display: flex;
flex-direction: row;
}
.justify-center {
justify-content: center;
}
.justify-space-around {
justify-content: space-around;
}
.justify-space-between {
justify-content: space-between;
}
.align-center {
align-items: center;
}
.mt-5 {
margin-top: 50px;
}
.mb-5 {
margin-bottom: 50px;
}
.mt-2 {
margin-top: 20px;
}
.mb-2 {
margin-bottom: 20px;
}
.mt-1 {
margin-top: 10px;
}
.mb-1 {
margin-bottom: 10px;
}
.pa-1 {
padding: 10px;
}
.pl-1 {
padding-left: 10px;
}
.pr-1 {
padding-right: 10px;
}
.pt-1 {
padding-top: 10px;
}
.pb-1 {
padding-bottom: 10px;
}
input[type="text"],
select {
padding: 8px;
font-size: 14px;
border: 1px solid #ececec;
outline: none;
transition: var(--transitionDuration);
color: #6e7071;
}
input[type="text"]:focus,
select:focus {
border: 1px solid #71b2c3;
}
input[name="resource-id-filter"], select[name="resource-type-filter"] {
width: 300px;
border-radius: 3px;
}
h1 {
font-size: 24px;
font-weight: bold;
}
h2 {
font-size: 20px;
font-weight: bold;
}
.heading-title {
display: block;
}
.heading-subtitle {
font-size: 14px;
display: block;
}
.app-content {
padding: 25px;
border-top: 3px solid #71B2C3;
background-color: #ffffff;
box-shadow: 0 0 5px #0000000a;
}
.resource-item {
border-top: 1px solid #ececec;
border-left: 1px solid #ececec;
border-right: 1px solid #ececec;
padding: 15px;
color: #6e7071;
font-size: 14px;
}
.resource-item:last-child {
border-bottom: 1px solid #ececec;
}
.resource-item:hover {
background-color: #f9f9f9;
}
.text--bold {
font-weight: bold;
}
.text--grey {
color: #747578;
}
.text--right {
text-align: right;
}
.card {
padding: 15px;
font-size: 15px;
background: #ffffff;
box-shadow: 0 0 5px #0000000a;
border-radius: 3px;
}
.reset-filter-btn {
border: none;
padding: 8px;
font-size: 14px;
background-color: transparent;
color: #5faabd;
cursor: pointer;
}
/* tabs style */
.tabs-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.tab {
cursor: pointer;
padding: 10px 20px;
margin: 0 2px;
background: transparent;
display: inline-block;
color: #747578;
transition: background-color 200ms;
border-radius: 3px;
}
.tab:hover {
background-color: #f9f9f9;
}
.panels {
width: 100%;
padding: 20px;
}
.panel {
display: none;
animation: fadein .8s;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.panel-title {
font-size: 1.5em;
font-weight: bold
}
.radio {
display: none;
}
#one:checked ~ .panels #one-panel,
#two:checked ~ .panels #two-panel,
#three:checked ~ .panels #three-panel,
#four:checked ~ .panels #four-panel {
display: block
}
#one:checked ~ .tabs #one-tab,
#two:checked ~ .tabs #two-tab,
#three:checked ~ .tabs #three-tab,
#four:checked ~ .tabs #four-tab {
background: #71b2c3;
color: #ffffff;
}
.empty-message-container {
color: #747578;
}</style>
</head>
<body>
<div class="container">
<div class="heading d-flex justify-space-between align-center">
<div>
<h1 class="heading-title mb-1">Driftctl scan report</h1>
<span class="heading-subtitle">Coverage 0%</span>
</div>
<div class="text--right">
<h2 class="heading-date mb-1">Jun 08, 2021</h2>
<span class="heading-subtitle">Scan duration 0s</span>
</div>
</div>
<div class="card d-flex justify-space-around mb-2 text--grey">
<span>Total resources: 0</span>
<span>Managed: 0%</span>
<span>Changed: 0/0</span>
<span>Unmanaged: 0%</span>
<span>Missing: 0%</span>
</div>
<div class="app-content">
<div class="d-flex justify-center mb-2">
<form id="filter-form" action="#">
<input type="text" name="resource-id-filter" placeholder="Search resources..." onkeyup="refreshState()">
<select name="resource-type-filter" onchange="refreshState()">
<option value="">Resource type</option>
</select>
<button type="button" onclick="resetFilters()" class="reset-filter-btn">Reset filters</button>
</form>
</div>
<div class="tabs-wrapper">
<input class="radio" id="one" name="group" type="radio" checked>
<input class="radio" id="two" name="group" type="radio">
<input class="radio" id="three" name="group" type="radio">
<input class="radio" id="four" name="group" type="radio">
<div class="tabs">
</div>
<div class="panels">
</div>
</div>
<div class="empty-message-container mt-5 d-flex justify-center hide">
<p>There's nothing to see there...</p>
</div>
</div>
</div>
</body>
<script lang="js">
const resources = document.querySelectorAll('.resource-item')
function hideResource(res) {
res.classList.add('hide')
}
function displayResource(res) {
res.classList.remove('hide')
}
function resourceIdContains(res, query) {
const el = res.querySelector('.resource-item-id')
if (!el) {
return false
}
return el.innerText.toLowerCase().includes(query.toLowerCase())
}
function resourceTypeEqual(res, type) {
const el = res.querySelector('.resource-item-type')
if (!el) {
return false
}
return el.innerText === type
}
function refreshCounters() {
const counterClassMapping = {
'.resource-item-unmanaged': '.resource-count-unmanaged',
'.resource-item-changed': '.resource-count-changed',
'.resource-item-deleted': '.resource-count-deleted',
'.resource-item-alerts': '.resource-count-alerts',
}
for (const resClass in counterClassMapping) {
const countEl = document.querySelector(counterClassMapping[resClass])
if (!countEl) {
continue
}
countEl.textContent = Array.from(document.querySelectorAll(resClass)).filter(el => !el.classList.contains('hide')).length
}
}
function computeEmptyMessage() {
const msgEl = document.querySelector('.empty-message-container')
const wrapperEl = document.querySelector('.tabs-wrapper')
const count = Array.from(resources).filter(el => !el.classList.contains('hide')).length
if (count === 0) {
msgEl.classList.remove('hide')
wrapperEl.classList.add('hide')
return
}
msgEl.classList.add('hide')
wrapperEl.classList.remove('hide')
}
function refreshState() {
const queryFilterInput = document.querySelector('input[name=resource-id-filter]').value
const typeFilterInput = document.querySelector('select[name=resource-type-filter]').value
for (const res of resources) {
const matchId = !queryFilterInput.length || resourceIdContains(res, queryFilterInput)
const matchType = !typeFilterInput.length || resourceTypeEqual(res, typeFilterInput)
if (matchId && matchType) {
displayResource(res)
continue
}
hideResource(res)
}
refreshCounters()
computeEmptyMessage()
}
function resetFilters() {
document.querySelector('input[name=resource-id-filter]').value = ""
document.querySelector('select[name=resource-type-filter]').value = ""
refreshState()
}
refreshState()
</script>
</html>

View File

@ -0,0 +1,428 @@
<!doctype html>
<html>
<head>
<title>Driftctl scan report</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="shortcut icon"
href="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/favicon.ico"/>
<style>/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/*
* Custom style
*/
:root {
--transitionDuration: 400ms;
}
body {
font-family: "Helvetica", sans-serif;
color: #1C1E21;
background-color: #F7F7F9;
padding-bottom: 50px;
}
div.container {
max-width: 100%;
width: 1280px;
margin: auto;
}
.heading {
height: 130px;
}
.hide {
display: none !important;
}
.d-flex {
display: flex;
flex-direction: row;
}
.justify-center {
justify-content: center;
}
.justify-space-around {
justify-content: space-around;
}
.justify-space-between {
justify-content: space-between;
}
.align-center {
align-items: center;
}
.mt-5 {
margin-top: 50px;
}
.mb-5 {
margin-bottom: 50px;
}
.mt-2 {
margin-top: 20px;
}
.mb-2 {
margin-bottom: 20px;
}
.mt-1 {
margin-top: 10px;
}
.mb-1 {
margin-bottom: 10px;
}
.pa-1 {
padding: 10px;
}
.pl-1 {
padding-left: 10px;
}
.pr-1 {
padding-right: 10px;
}
.pt-1 {
padding-top: 10px;
}
.pb-1 {
padding-bottom: 10px;
}
input[type="text"],
select {
padding: 8px;
font-size: 14px;
border: 1px solid #ececec;
outline: none;
transition: var(--transitionDuration);
color: #6e7071;
}
input[type="text"]:focus,
select:focus {
border: 1px solid #71b2c3;
}
input[name="resource-id-filter"], select[name="resource-type-filter"] {
width: 300px;
border-radius: 3px;
}
h1 {
font-size: 24px;
font-weight: bold;
}
h2 {
font-size: 20px;
font-weight: bold;
}
.heading-title {
display: block;
}
.heading-subtitle {
font-size: 14px;
display: block;
}
.app-content {
padding: 25px;
border-top: 3px solid #71B2C3;
background-color: #ffffff;
box-shadow: 0 0 5px #0000000a;
}
.resource-item {
border-top: 1px solid #ececec;
border-left: 1px solid #ececec;
border-right: 1px solid #ececec;
padding: 15px;
color: #6e7071;
font-size: 14px;
}
.resource-item:last-child {
border-bottom: 1px solid #ececec;
}
.resource-item:hover {
background-color: #f9f9f9;
}
.text--bold {
font-weight: bold;
}
.text--grey {
color: #747578;
}
.text--right {
text-align: right;
}
.card {
padding: 15px;
font-size: 15px;
background: #ffffff;
box-shadow: 0 0 5px #0000000a;
border-radius: 3px;
}
.reset-filter-btn {
border: none;
padding: 8px;
font-size: 14px;
background-color: transparent;
color: #5faabd;
cursor: pointer;
}
/* tabs style */
.tabs-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.tab {
cursor: pointer;
padding: 10px 20px;
margin: 0 2px;
background: transparent;
display: inline-block;
color: #747578;
transition: background-color 200ms;
border-radius: 3px;
}
.tab:hover {
background-color: #f9f9f9;
}
.panels {
width: 100%;
padding: 20px;
}
.panel {
display: none;
animation: fadein .8s;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.panel-title {
font-size: 1.5em;
font-weight: bold
}
.radio {
display: none;
}
#one:checked ~ .panels #one-panel,
#two:checked ~ .panels #two-panel,
#three:checked ~ .panels #three-panel,
#four:checked ~ .panels #four-panel {
display: block
}
#one:checked ~ .tabs #one-tab,
#two:checked ~ .tabs #two-tab,
#three:checked ~ .tabs #three-tab,
#four:checked ~ .tabs #four-tab {
background: #71b2c3;
color: #ffffff;
}
.empty-message-container {
color: #747578;
}</style>
</head>
<body>
<div class="container">
<div class="heading d-flex justify-space-between align-center">
<div>
<h1 class="heading-title mb-1">Driftctl scan report</h1>
<span class="heading-subtitle">Coverage 100%</span>
</div>
<div class="text--right">
<h2 class="heading-date mb-1">Jun 08, 2021</h2>
<span class="heading-subtitle">Scan duration 1m12s</span>
</div>
</div>
<div class="card d-flex justify-space-around mb-2 text--grey">
<span>Total resources: 1</span>
<span>Managed: 100%</span>
<span>Changed: 0/1</span>
<span>Unmanaged: 0%</span>
<span>Missing: 0%</span>
</div>
<div class="app-content">
<div class="d-flex justify-center mt-5 mb-5">
<h1>Congrats! Your infrastructure is in sync</h1>
</div>
</div>
</div>
</body>
<script lang="js">
const resources = document.querySelectorAll('.resource-item')
function hideResource(res) {
res.classList.add('hide')
}
function displayResource(res) {
res.classList.remove('hide')
}
function resourceIdContains(res, query) {
const el = res.querySelector('.resource-item-id')
if (!el) {
return false
}
return el.innerText.toLowerCase().includes(query.toLowerCase())
}
function resourceTypeEqual(res, type) {
const el = res.querySelector('.resource-item-type')
if (!el) {
return false
}
return el.innerText === type
}
function refreshCounters() {
const counterClassMapping = {
'.resource-item-unmanaged': '.resource-count-unmanaged',
'.resource-item-changed': '.resource-count-changed',
'.resource-item-deleted': '.resource-count-deleted',
'.resource-item-alerts': '.resource-count-alerts',
}
for (const resClass in counterClassMapping) {
const countEl = document.querySelector(counterClassMapping[resClass])
if (!countEl) {
continue
}
countEl.textContent = Array.from(document.querySelectorAll(resClass)).filter(el => !el.classList.contains('hide')).length
}
}
function computeEmptyMessage() {
const msgEl = document.querySelector('.empty-message-container')
const wrapperEl = document.querySelector('.tabs-wrapper')
const count = Array.from(resources).filter(el => !el.classList.contains('hide')).length
if (count === 0) {
msgEl.classList.remove('hide')
wrapperEl.classList.add('hide')
return
}
msgEl.classList.add('hide')
wrapperEl.classList.remove('hide')
}
function refreshState() {
const queryFilterInput = document.querySelector('input[name=resource-id-filter]').value
const typeFilterInput = document.querySelector('select[name=resource-type-filter]').value
for (const res of resources) {
const matchId = !queryFilterInput.length || resourceIdContains(res, queryFilterInput)
const matchType = !typeFilterInput.length || resourceTypeEqual(res, typeFilterInput)
if (matchId && matchType) {
displayResource(res)
continue
}
hideResource(res)
}
refreshCounters()
computeEmptyMessage()
}
function resetFilters() {
document.querySelector('input[name=resource-id-filter]').value = ""
document.querySelector('select[name=resource-type-filter]').value = ""
refreshState()
}
refreshState()
</script>
</html>