mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
ui: Display server configuration on status page
This commit is contained in:
@@ -3,13 +3,19 @@
|
|||||||
# description: Developer friendly Inbucket configuration
|
# description: Developer friendly Inbucket configuration
|
||||||
|
|
||||||
export INBUCKET_LOGLEVEL="debug"
|
export INBUCKET_LOGLEVEL="debug"
|
||||||
|
export INBUCKET_SMTP_REJECTDOMAINS="bad-actors.local"
|
||||||
|
#export INBUCKET_SMTP_DEFAULTACCEPT="false"
|
||||||
|
export INBUCKET_SMTP_ACCEPTDOMAINS="good-actors.local"
|
||||||
export INBUCKET_SMTP_DISCARDDOMAINS="bitbucket.local"
|
export INBUCKET_SMTP_DISCARDDOMAINS="bitbucket.local"
|
||||||
|
#export INBUCKET_SMTP_DEFAULTSTORE="false"
|
||||||
|
export INBUCKET_SMTP_STOREDOMAINS="important.local"
|
||||||
export INBUCKET_WEB_TEMPLATECACHE="false"
|
export INBUCKET_WEB_TEMPLATECACHE="false"
|
||||||
export INBUCKET_WEB_COOKIEAUTHKEY="not-secret"
|
export INBUCKET_WEB_COOKIEAUTHKEY="not-secret"
|
||||||
export INBUCKET_WEB_UIDIR="ui/dist"
|
export INBUCKET_WEB_UIDIR="ui/dist"
|
||||||
export INBUCKET_STORAGE_TYPE="file"
|
export INBUCKET_STORAGE_TYPE="file"
|
||||||
export INBUCKET_STORAGE_PARAMS="path:/tmp/inbucket"
|
export INBUCKET_STORAGE_PARAMS="path:/tmp/inbucket"
|
||||||
export INBUCKET_STORAGE_RETENTIONPERIOD="3h"
|
export INBUCKET_STORAGE_RETENTIONPERIOD="3h"
|
||||||
|
export INBUCKET_STORAGE_MAILBOXMSGCAP="300"
|
||||||
|
|
||||||
if ! test -x ./inbucket; then
|
if ! test -x ./inbucket; then
|
||||||
echo "$PWD/inbucket not found/executable!" >&2
|
echo "$PWD/inbucket not found/executable!" >&2
|
||||||
|
|||||||
@@ -71,21 +71,31 @@ func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *web.Conte
|
|||||||
|
|
||||||
// RootStatus serves the Inbucket status page
|
// RootStatus serves the Inbucket status page
|
||||||
func RootStatus(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
func RootStatus(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
||||||
// Get flash messages, save session
|
root := ctx.RootConfig
|
||||||
errorFlash := ctx.Session.Flashes("errors")
|
retPeriod := ""
|
||||||
if err = ctx.Session.Save(req, w); err != nil {
|
if root.Storage.RetentionPeriod > 0 {
|
||||||
return err
|
retPeriod = root.Storage.RetentionPeriod.String()
|
||||||
}
|
}
|
||||||
// Render template
|
|
||||||
return web.RenderTemplate("root/status.html", w, map[string]interface{}{
|
return web.RenderJSON(w,
|
||||||
"ctx": ctx,
|
&jsonServerConfig{
|
||||||
"errorFlash": errorFlash,
|
Version: config.Version,
|
||||||
"version": config.Version,
|
BuildDate: config.BuildDate,
|
||||||
"buildDate": config.BuildDate,
|
POP3Listener: root.POP3.Addr,
|
||||||
"smtpListener": ctx.RootConfig.SMTP.Addr,
|
WebListener: root.Web.Addr,
|
||||||
"pop3Listener": ctx.RootConfig.POP3.Addr,
|
SMTPConfig: jsonSMTPConfig{
|
||||||
"webListener": ctx.RootConfig.Web.Addr,
|
Addr: root.SMTP.Addr,
|
||||||
"smtpConfig": ctx.RootConfig.SMTP,
|
DefaultAccept: root.SMTP.DefaultAccept,
|
||||||
"storageConfig": ctx.RootConfig.Storage,
|
AcceptDomains: root.SMTP.AcceptDomains,
|
||||||
})
|
RejectDomains: root.SMTP.RejectDomains,
|
||||||
|
DefaultStore: root.SMTP.DefaultStore,
|
||||||
|
StoreDomains: root.SMTP.StoreDomains,
|
||||||
|
DiscardDomains: root.SMTP.DiscardDomains,
|
||||||
|
},
|
||||||
|
StorageConfig: jsonStorageConfig{
|
||||||
|
MailboxMsgCap: root.Storage.MailboxMsgCap,
|
||||||
|
StoreType: root.Storage.Type,
|
||||||
|
RetentionPeriod: retPeriod,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
26
pkg/webui/status_json.go
Normal file
26
pkg/webui/status_json.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package webui
|
||||||
|
|
||||||
|
type jsonServerConfig struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
BuildDate string `json:"build-date"`
|
||||||
|
POP3Listener string `json:"pop3-listener"`
|
||||||
|
WebListener string `json:"web-listener"`
|
||||||
|
SMTPConfig jsonSMTPConfig `json:"smtp-config"`
|
||||||
|
StorageConfig jsonStorageConfig `json:"storage-config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonSMTPConfig struct {
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
DefaultAccept bool `json:"default-accept"`
|
||||||
|
AcceptDomains []string `json:"accept-domains"`
|
||||||
|
RejectDomains []string `json:"reject-domains"`
|
||||||
|
DefaultStore bool `json:"default-store"`
|
||||||
|
StoreDomains []string `json:"store-domains"`
|
||||||
|
DiscardDomains []string `json:"discard-domains"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonStorageConfig struct {
|
||||||
|
MailboxMsgCap int `json:"mailbox-msg-cap"`
|
||||||
|
StoreType string `json:"store-type"`
|
||||||
|
RetentionPeriod string `json:"retention-period"`
|
||||||
|
}
|
||||||
2
ui/dist/static/main.js
vendored
2
ui/dist/static/main.js
vendored
File diff suppressed because one or more lines are too long
107
ui/src/Data/ServerConfig.elm
Normal file
107
ui/src/Data/ServerConfig.elm
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
module Data.ServerConfig exposing (ServerConfig, decoder, encode)
|
||||||
|
|
||||||
|
import Json.Decode as D
|
||||||
|
import Json.Decode.Pipeline as P
|
||||||
|
import Json.Encode as E
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Generated by https://github.com/jhillyerd/go-to-elm-json
|
||||||
|
|
||||||
|
|
||||||
|
type alias ServerConfig =
|
||||||
|
{ version : String
|
||||||
|
, buildDate : String
|
||||||
|
, pop3Listener : String
|
||||||
|
, webListener : String
|
||||||
|
, smtpConfig : SmtpConfig
|
||||||
|
, storageConfig : StorageConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias SmtpConfig =
|
||||||
|
{ addr : String
|
||||||
|
, defaultAccept : Bool
|
||||||
|
, acceptDomains : Maybe (List String)
|
||||||
|
, rejectDomains : Maybe (List String)
|
||||||
|
, defaultStore : Bool
|
||||||
|
, storeDomains : Maybe (List String)
|
||||||
|
, discardDomains : Maybe (List String)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias StorageConfig =
|
||||||
|
{ mailboxMsgCap : Int
|
||||||
|
, storeType : String
|
||||||
|
, retentionPeriod : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
decoder : D.Decoder ServerConfig
|
||||||
|
decoder =
|
||||||
|
D.succeed ServerConfig
|
||||||
|
|> P.required "version" D.string
|
||||||
|
|> P.required "build-date" D.string
|
||||||
|
|> P.required "pop3-listener" D.string
|
||||||
|
|> P.required "web-listener" D.string
|
||||||
|
|> P.required "smtp-config" smtpConfigDecoder
|
||||||
|
|> P.required "storage-config" storageConfigDecoder
|
||||||
|
|
||||||
|
|
||||||
|
encode : ServerConfig -> E.Value
|
||||||
|
encode r =
|
||||||
|
E.object
|
||||||
|
[ ( "version", E.string r.version )
|
||||||
|
, ( "build-date", E.string r.buildDate )
|
||||||
|
, ( "pop3-listener", E.string r.pop3Listener )
|
||||||
|
, ( "web-listener", E.string r.webListener )
|
||||||
|
, ( "smtp-config", encodeSmtpConfig r.smtpConfig )
|
||||||
|
, ( "storage-config", encodeStorageConfig r.storageConfig )
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
smtpConfigDecoder : D.Decoder SmtpConfig
|
||||||
|
smtpConfigDecoder =
|
||||||
|
D.succeed SmtpConfig
|
||||||
|
|> P.required "addr" D.string
|
||||||
|
|> P.required "default-accept" D.bool
|
||||||
|
|> P.required "accept-domains" (D.nullable (D.list D.string))
|
||||||
|
|> P.required "reject-domains" (D.nullable (D.list D.string))
|
||||||
|
|> P.required "default-store" D.bool
|
||||||
|
|> P.required "store-domains" (D.nullable (D.list D.string))
|
||||||
|
|> P.required "discard-domains" (D.nullable (D.list D.string))
|
||||||
|
|
||||||
|
|
||||||
|
encodeSmtpConfig : SmtpConfig -> E.Value
|
||||||
|
encodeSmtpConfig r =
|
||||||
|
E.object
|
||||||
|
[ ( "addr", E.string r.addr )
|
||||||
|
, ( "default-accept", E.bool r.defaultAccept )
|
||||||
|
, ( "accept-domains", maybe (E.list E.string) r.acceptDomains )
|
||||||
|
, ( "reject-domains", maybe (E.list E.string) r.rejectDomains )
|
||||||
|
, ( "default-store", E.bool r.defaultStore )
|
||||||
|
, ( "store-domains", maybe (E.list E.string) r.storeDomains )
|
||||||
|
, ( "discard-domains", maybe (E.list E.string) r.discardDomains )
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
storageConfigDecoder : D.Decoder StorageConfig
|
||||||
|
storageConfigDecoder =
|
||||||
|
D.succeed StorageConfig
|
||||||
|
|> P.required "mailbox-msg-cap" D.int
|
||||||
|
|> P.required "store-type" D.string
|
||||||
|
|> P.required "retention-period" D.string
|
||||||
|
|
||||||
|
|
||||||
|
encodeStorageConfig : StorageConfig -> E.Value
|
||||||
|
encodeStorageConfig r =
|
||||||
|
E.object
|
||||||
|
[ ( "mailbox-msg-cap", E.int r.mailboxMsgCap )
|
||||||
|
, ( "store-type", E.string r.storeType )
|
||||||
|
, ( "retention-period", E.string r.retentionPeriod )
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
maybe : (a -> E.Value) -> Maybe a -> E.Value
|
||||||
|
maybe encoder =
|
||||||
|
Maybe.map encoder >> Maybe.withDefault E.null
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
module Page.Status exposing (Model, Msg, init, subscriptions, update, view)
|
module Page.Status exposing (Model, Msg, init, subscriptions, update, view)
|
||||||
|
|
||||||
import Data.Metrics as Metrics exposing (Metrics)
|
import Data.Metrics as Metrics exposing (Metrics)
|
||||||
|
import Data.ServerConfig as ServerConfig exposing (ServerConfig)
|
||||||
import Data.Session as Session exposing (Session)
|
import Data.Session as Session exposing (Session)
|
||||||
import Filesize
|
import Filesize
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
@@ -17,7 +18,8 @@ import Time exposing (Posix)
|
|||||||
|
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
{ metrics : Maybe Metrics
|
{ config : Maybe ServerConfig
|
||||||
|
, metrics : Maybe Metrics
|
||||||
, xCounter : Float
|
, xCounter : Float
|
||||||
, sysMem : Metric
|
, sysMem : Metric
|
||||||
, heapSize : Metric
|
, heapSize : Metric
|
||||||
@@ -48,7 +50,8 @@ type alias Metric =
|
|||||||
|
|
||||||
init : ( Model, Cmd Msg, Session.Msg )
|
init : ( Model, Cmd Msg, Session.Msg )
|
||||||
init =
|
init =
|
||||||
( { metrics = Nothing
|
( { config = Nothing
|
||||||
|
, metrics = Nothing
|
||||||
, xCounter = 60
|
, xCounter = 60
|
||||||
, sysMem = Metric "System Memory" 0 Filesize.format graphZero initDataSet 10
|
, sysMem = Metric "System Memory" 0 Filesize.format graphZero initDataSet 10
|
||||||
, heapSize = Metric "Heap Size" 0 Filesize.format graphZero initDataSet 10
|
, heapSize = Metric "Heap Size" 0 Filesize.format graphZero initDataSet 10
|
||||||
@@ -65,7 +68,7 @@ init =
|
|||||||
, retainedCount = Metric "Stored Messages" 0 fmtInt graphZero initDataSet 60
|
, retainedCount = Metric "Stored Messages" 0 fmtInt graphZero initDataSet 60
|
||||||
, retainedSize = Metric "Store Size" 0 Filesize.format graphZero initDataSet 60
|
, retainedSize = Metric "Store Size" 0 Filesize.format graphZero initDataSet 60
|
||||||
}
|
}
|
||||||
, getMetrics
|
, Cmd.batch [ loadServerConfig, loadMetrics ]
|
||||||
, Session.none
|
, Session.none
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,6 +94,7 @@ subscriptions model =
|
|||||||
|
|
||||||
type Msg
|
type Msg
|
||||||
= MetricsReceived (Result Http.Error Metrics)
|
= MetricsReceived (Result Http.Error Metrics)
|
||||||
|
| ServerConfigLoaded (Result Http.Error ServerConfig)
|
||||||
| Tick Posix
|
| Tick Posix
|
||||||
|
|
||||||
|
|
||||||
@@ -103,8 +107,14 @@ update session msg model =
|
|||||||
MetricsReceived (Err err) ->
|
MetricsReceived (Err err) ->
|
||||||
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||||
|
|
||||||
|
ServerConfigLoaded (Ok config) ->
|
||||||
|
( { model | config = Just config }, Cmd.none, Session.none )
|
||||||
|
|
||||||
|
ServerConfigLoaded (Err err) ->
|
||||||
|
( model, Cmd.none, Session.SetFlash (HttpUtil.errorString err) )
|
||||||
|
|
||||||
Tick time ->
|
Tick time ->
|
||||||
( model, getMetrics, Session.ClearFlash )
|
( model, loadMetrics, Session.none )
|
||||||
|
|
||||||
|
|
||||||
{-| Update all metrics in Model; increment xCounter.
|
{-| Update all metrics in Model; increment xCounter.
|
||||||
@@ -203,14 +213,22 @@ updateRemoteTotal metric value history =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getMetrics : Cmd Msg
|
loadMetrics : Cmd Msg
|
||||||
getMetrics =
|
loadMetrics =
|
||||||
Http.get
|
Http.get
|
||||||
{ url = "/debug/vars"
|
{ url = "/debug/vars"
|
||||||
, expect = Http.expectJson MetricsReceived Metrics.decoder
|
, expect = Http.expectJson MetricsReceived Metrics.decoder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
loadServerConfig : Cmd Msg
|
||||||
|
loadServerConfig =
|
||||||
|
Http.get
|
||||||
|
{ url = "/serve/status"
|
||||||
|
, expect = Http.expectJson ServerConfigLoaded ServerConfig.decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- VIEW --
|
-- VIEW --
|
||||||
|
|
||||||
@@ -221,37 +239,142 @@ view session model =
|
|||||||
, content =
|
, content =
|
||||||
div [ class "page" ]
|
div [ class "page" ]
|
||||||
[ h1 [] [ text "Status" ]
|
[ h1 [] [ text "Status" ]
|
||||||
, case model.metrics of
|
, div [] (configPanel model.config :: metricPanels model)
|
||||||
Nothing ->
|
|
||||||
div [] [ text "Loading metrics..." ]
|
|
||||||
|
|
||||||
Just metrics ->
|
|
||||||
div []
|
|
||||||
[ framePanel "General Metrics"
|
|
||||||
[ viewMetric model.sysMem
|
|
||||||
, viewMetric model.heapSize
|
|
||||||
, viewMetric model.heapUsed
|
|
||||||
, viewMetric model.heapObjects
|
|
||||||
, viewMetric model.goRoutines
|
|
||||||
, viewMetric model.webSockets
|
|
||||||
]
|
|
||||||
, framePanel "SMTP Metrics"
|
|
||||||
[ viewMetric model.smtpConnOpen
|
|
||||||
, viewMetric model.smtpConnTotal
|
|
||||||
, viewMetric model.smtpReceivedTotal
|
|
||||||
, viewMetric model.smtpErrorsTotal
|
|
||||||
, viewMetric model.smtpWarnsTotal
|
|
||||||
]
|
|
||||||
, framePanel "Storage Metrics"
|
|
||||||
[ viewMetric model.retentionDeletesTotal
|
|
||||||
, viewMetric model.retainedCount
|
|
||||||
, viewMetric model.retainedSize
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
configPanel : Maybe ServerConfig -> Html Msg
|
||||||
|
configPanel maybeConfig =
|
||||||
|
let
|
||||||
|
mailboxCap config =
|
||||||
|
case config.storageConfig.mailboxMsgCap of
|
||||||
|
0 ->
|
||||||
|
"Unlimited"
|
||||||
|
|
||||||
|
cap ->
|
||||||
|
String.fromInt cap ++ " messages per mailbox"
|
||||||
|
|
||||||
|
retentionPeriod config =
|
||||||
|
case config.storageConfig.retentionPeriod of
|
||||||
|
"" ->
|
||||||
|
"Forever"
|
||||||
|
|
||||||
|
period ->
|
||||||
|
period
|
||||||
|
in
|
||||||
|
case maybeConfig of
|
||||||
|
Nothing ->
|
||||||
|
text "Loading server config..."
|
||||||
|
|
||||||
|
Just config ->
|
||||||
|
framePanel "Configuration"
|
||||||
|
[ textEntry "Version" (config.version ++ ", built on " ++ config.buildDate)
|
||||||
|
, textEntry "SMTP Listener" config.smtpConfig.addr
|
||||||
|
, textEntry "POP3 Listener" config.pop3Listener
|
||||||
|
, textEntry "HTTP Listener" config.webListener
|
||||||
|
, textEntry "Accept Policy" (acceptPolicy config.smtpConfig)
|
||||||
|
, textEntry "Store Policy" (storePolicy config.smtpConfig)
|
||||||
|
, textEntry "Store Type" config.storageConfig.storeType
|
||||||
|
, textEntry "Message Cap" (mailboxCap config)
|
||||||
|
, textEntry "Retention Period" (retentionPeriod config)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
acceptPolicy config =
|
||||||
|
if config.defaultAccept then
|
||||||
|
"All domains"
|
||||||
|
++ (case config.rejectDomains of
|
||||||
|
Nothing ->
|
||||||
|
""
|
||||||
|
|
||||||
|
Just [] ->
|
||||||
|
""
|
||||||
|
|
||||||
|
Just domains ->
|
||||||
|
", except: " ++ String.join ", " domains
|
||||||
|
)
|
||||||
|
|
||||||
|
else
|
||||||
|
"No domains"
|
||||||
|
++ (case config.acceptDomains of
|
||||||
|
Nothing ->
|
||||||
|
""
|
||||||
|
|
||||||
|
Just [] ->
|
||||||
|
""
|
||||||
|
|
||||||
|
Just domains ->
|
||||||
|
", except: " ++ String.join ", " domains
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
storePolicy config =
|
||||||
|
if config.defaultStore then
|
||||||
|
"All domains"
|
||||||
|
++ (case config.discardDomains of
|
||||||
|
Nothing ->
|
||||||
|
""
|
||||||
|
|
||||||
|
Just [] ->
|
||||||
|
""
|
||||||
|
|
||||||
|
Just domains ->
|
||||||
|
", except: " ++ String.join ", " domains
|
||||||
|
)
|
||||||
|
|
||||||
|
else
|
||||||
|
"No domains"
|
||||||
|
++ (case config.storeDomains of
|
||||||
|
Nothing ->
|
||||||
|
""
|
||||||
|
|
||||||
|
Just [] ->
|
||||||
|
""
|
||||||
|
|
||||||
|
Just domains ->
|
||||||
|
", except: " ++ String.join ", " domains
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
metricPanels : Model -> List (Html Msg)
|
||||||
|
metricPanels model =
|
||||||
|
case model.metrics of
|
||||||
|
Nothing ->
|
||||||
|
[ text "Loading metrics..." ]
|
||||||
|
|
||||||
|
Just _ ->
|
||||||
|
[ framePanel "General Metrics"
|
||||||
|
[ viewMetric model.sysMem
|
||||||
|
, viewMetric model.heapSize
|
||||||
|
, viewMetric model.heapUsed
|
||||||
|
, viewMetric model.heapObjects
|
||||||
|
, viewMetric model.goRoutines
|
||||||
|
, viewMetric model.webSockets
|
||||||
|
]
|
||||||
|
, framePanel "SMTP Metrics"
|
||||||
|
[ viewMetric model.smtpConnOpen
|
||||||
|
, viewMetric model.smtpConnTotal
|
||||||
|
, viewMetric model.smtpReceivedTotal
|
||||||
|
, viewMetric model.smtpErrorsTotal
|
||||||
|
, viewMetric model.smtpWarnsTotal
|
||||||
|
]
|
||||||
|
, framePanel "Storage Metrics"
|
||||||
|
[ viewMetric model.retentionDeletesTotal
|
||||||
|
, viewMetric model.retainedCount
|
||||||
|
, viewMetric model.retainedSize
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
textEntry : String -> String -> Html Msg
|
||||||
|
textEntry name value =
|
||||||
|
div [ class "metric" ]
|
||||||
|
[ div [ class "label" ] [ text name ]
|
||||||
|
, div [ class "text-value" ] [ text value ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
viewMetric : Metric -> Html Msg
|
viewMetric : Metric -> Html Msg
|
||||||
viewMetric metric =
|
viewMetric metric =
|
||||||
div [ class "metric" ]
|
div [ class "metric" ]
|
||||||
|
|||||||
@@ -462,6 +462,10 @@ nav.tab-bar a.active:hover {
|
|||||||
flex-basis: 15em;
|
flex-basis: 15em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric .text-value {
|
||||||
|
flex-basis: 40em;
|
||||||
|
}
|
||||||
|
|
||||||
.metric .graph {
|
.metric .graph {
|
||||||
flex-basis: 25em;
|
flex-basis: 25em;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user