This commit is contained in:
Jason Kulatunga
2020-08-19 16:04:21 -07:00
commit 8482272d45
336 changed files with 197309 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
package main
import (
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
"github.com/analogj/scrutiny/webapp/backend/pkg/web"
log "github.com/sirupsen/logrus"
"os"
"time"
utils "github.com/analogj/go-util/utils"
"github.com/fatih/color"
"github.com/urfave/cli/v2"
)
var goos string
var goarch string
func main() {
config, err := config.Create()
if err != nil {
fmt.Printf("FATAL: %+v\n", err)
os.Exit(1)
}
//we're going to load the config file manually, since we need to validate it.
err = config.ReadConfig("/scrutiny/config/scrutiny.yaml") // Find and read the config file
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
//ignore "could not find config file"
} else if err != nil {
os.Exit(1)
}
cli.CommandHelpTemplate = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
CATEGORY:
{{.Category}}{{end}}{{if .Description}}
DESCRIPTION:
{{.Description}}{{end}}{{if .VisibleFlags}}
OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
`
app := &cli.App{
Name: "scrutiny",
Usage: "WebUI for smartd S.M.A.R.T monitoring",
Version: version.VERSION,
Compiled: time.Now(),
Authors: []*cli.Author{
{
Name: "Jason Kulatunga",
Email: "jason@thesparktree.com",
},
},
Before: func(c *cli.Context) error {
drawbridge := "github.com/AnalogJ/scrutiny"
var versionInfo string
if len(goos) > 0 && len(goarch) > 0 {
versionInfo = fmt.Sprintf("%s.%s-%s", goos, goarch, version.VERSION)
} else {
versionInfo = fmt.Sprintf("dev-%s", version.VERSION)
}
subtitle := drawbridge + utils.LeftPad2Len(versionInfo, " ", 65-len(drawbridge))
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
`
___ ___ ____ __ __ ____ ____ _ _ _ _
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
\__ \( (__ ) / )(__)( )( _)(_ ) ( \ /
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
%s
`), subtitle))
return nil
},
Commands: []*cli.Command{
{
Name: "start",
Usage: "Start the scrutiny server",
Action: func(c *cli.Context) error {
fmt.Fprintln(c.App.Writer, c.Command.Usage)
if c.IsSet("config") {
err = config.ReadConfig(c.String("config")) // Find and read the config file
if err != nil { // Handle errors reading the config file
//ignore "could not find config file"
fmt.Printf("Could not find config file at specified path: %s", c.String("config"))
os.Exit(1)
}
}
webServer := web.AppEngine{Config: config}
return webServer.Start()
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Usage: "Specify the path to the config file",
},
},
},
},
}
err = app.Run(os.Args)
if err != nil {
log.Fatal(color.HiRedString("ERROR: %v", err))
}
}
+116
View File
@@ -0,0 +1,116 @@
package config
import (
"github.com/analogj/go-util/utils"
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
"github.com/spf13/viper"
"log"
"os"
)
// When initializing this class the following methods must be called:
// Config.New
// Config.Init
// This is done automatically when created via the Factory.
type configuration struct {
*viper.Viper
}
//Viper uses the following precedence order. Each item takes precedence over the item below it:
// explicit call to Set
// flag
// env
// config
// key/value store
// default
func (c *configuration) Init() error {
c.Viper = viper.New()
//set defaults
c.SetDefault("web.listen.port", "8080")
c.SetDefault("web.listen.host", "0.0.0.0")
c.SetDefault("web.src.frontend.path", "/scrutiny/web")
c.SetDefault("web.database.location", "/scrutiny/config/scrutiny.db")
c.SetDefault("disks.include", []string{})
c.SetDefault("disks.exclude", []string{})
c.SetDefault("notify.metric.script", "/scrutiny/config/notify-metrics.sh")
c.SetDefault("notify.long.script", "/scrutiny/config/notify-long-test.sh")
c.SetDefault("notify.short.script", "/scrutiny/config/notify-short-test.sh")
c.SetDefault("collect.metric.enable", true)
c.SetDefault("collect.metric.command", "-a -o on -S on")
c.SetDefault("collect.long.enable", true)
c.SetDefault("collect.long.command", "-a -o on -S on")
c.SetDefault("collect.short.enable", true)
c.SetDefault("collect.short.command", "-a -o on -S on")
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
c.SetConfigType("yaml")
//c.SetConfigName("drawbridge")
//c.AddConfigPath("$HOME/")
//CLI options will be added via the `Set()` function
return nil
}
func (c *configuration) ReadConfig(configFilePath string) error {
configFilePath, err := utils.ExpandPath(configFilePath)
if err != nil {
return err
}
if !utils.FileExists(configFilePath) {
log.Printf("No configuration file found at %v. Skipping", configFilePath)
return errors.ConfigFileMissingError("The configuration file could not be found.")
}
//validate config file contents
//err = c.ValidateConfigFile(configFilePath)
//if err != nil {
// log.Printf("Config file at `%v` is invalid: %s", configFilePath, err)
// return err
//}
log.Printf("Loading configuration file: %s", configFilePath)
config_data, err := os.Open(configFilePath)
if err != nil {
log.Printf("Error reading configuration file: %s", err)
return err
}
err = c.MergeConfig(config_data)
if err != nil {
return err
}
return c.ValidateConfig()
}
// This function ensures that the merged config works correctly.
func (c *configuration) ValidateConfig() error {
////deserialize Questions
//questionsMap := map[string]Question{}
//err := c.UnmarshalKey("questions", &questionsMap)
//
//if err != nil {
// log.Printf("questions could not be deserialized correctly. %v", err)
// return err
//}
//
//for _, v := range questionsMap {
//
// typeContent, ok := v.Schema["type"].(string)
// if !ok || len(typeContent) == 0 {
// return errors.QuestionSyntaxError("`type` is required for questions")
// }
//}
//
//
return nil
}
+9
View File
@@ -0,0 +1,9 @@
package config
func Create() (Interface, error) {
config := new(configuration)
if err := config.Init(); err != nil {
return nil, err
}
return config, nil
}
+23
View File
@@ -0,0 +1,23 @@
package config
import (
"github.com/spf13/viper"
)
// Create mock using:
// mockgen -source=pkg/config/interface.go -destination=pkg/config/mock/mock_config.go
type Interface interface {
Init() error
ReadConfig(configFilePath string) error
Set(key string, value interface{})
SetDefault(key string, value interface{})
AllSettings() map[string]interface{}
IsSet(key string) bool
Get(key string) interface{}
GetBool(key string) bool
GetInt(key string) int
GetString(key string) string
GetStringSlice(key string) []string
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
}
+30
View File
@@ -0,0 +1,30 @@
package database
import (
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
func DatabaseHandler(dbPath string) gin.HandlerFunc {
//var database *gorm.DB
fmt.Printf("Trying to connect to database stored: %s", dbPath)
database, err := gorm.Open("sqlite3", dbPath)
if err != nil {
panic("Failed to connect to database!")
}
database.AutoMigrate(&db.Device{})
database.AutoMigrate(&db.SelfTest{})
database.AutoMigrate(&db.Smart{})
database.AutoMigrate(&db.SmartAttribute{})
//TODO: detrmine where we can call defer database.Close()
return func(c *gin.Context) {
c.Set("DB", database)
c.Next()
}
}
+26
View File
@@ -0,0 +1,26 @@
package errors
import (
"fmt"
)
// Raised when config file is missing
type ConfigFileMissingError string
func (str ConfigFileMissingError) Error() string {
return fmt.Sprintf("ConfigFileMissingError: %q", string(str))
}
// Raised when the config file doesnt match schema
type ConfigValidationError string
func (str ConfigValidationError) Error() string {
return fmt.Sprintf("ConfigValidationError: %q", string(str))
}
// Raised when a dependency (like smartd or ssh-agent) is missing
type DependencyMissingError string
func (str DependencyMissingError) Error() string {
return fmt.Sprintf("DependencyMissingError: %q", string(str))
}
+34
View File
@@ -0,0 +1,34 @@
package errors_test
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
"github.com/stretchr/testify/require"
"testing"
)
//func TestCheckErr_WithoutError(t *testing.T) {
// t.Parallel()
//
// //assert
// require.NotPanics(t, func() {
// errors.CheckErr(nil)
// })
//}
//func TestCheckErr_Error(t *testing.T) {
// t.Parallel()
//
// //assert
// require.Panics(t, func() {
// errors.CheckErr(stderrors.New("This is an error"))
// })
//}
func TestErrors(t *testing.T) {
t.Parallel()
//assert
require.Implements(t, (*error)(nil), errors.ConfigFileMissingError("test"), "should implement the error interface")
require.Implements(t, (*error)(nil), errors.ConfigValidationError("test"), "should implement the error interface")
require.Implements(t, (*error)(nil), errors.DependencyMissingError("test"), "should implement the error interface")
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,186 @@
package collector
type SmartInfo struct {
JSONFormatVersion []int `json:"json_format_version"`
Smartctl struct {
Version []int `json:"version"`
SvnRevision string `json:"svn_revision"`
PlatformInfo string `json:"platform_info"`
BuildInfo string `json:"build_info"`
Argv []string `json:"argv"`
ExitStatus int `json:"exit_status"`
Messages []struct {
String string `json:"string"`
Severity string `json:"severity"`
} `json:"messages"`
} `json:"smartctl"`
Device struct {
Name string `json:"name"`
InfoName string `json:"info_name"`
Type string `json:"type"`
Protocol string `json:"protocol"`
} `json:"device"`
ModelName string `json:"model_name"`
SerialNumber string `json:"serial_number"`
Wwn struct {
Naa int `json:"naa"`
Oui int `json:"oui"`
ID int64 `json:"id"`
} `json:"wwn"`
FirmwareVersion string `json:"firmware_version"`
UserCapacity struct {
Blocks int64 `json:"blocks"`
Bytes int64 `json:"bytes"`
} `json:"user_capacity"`
LogicalBlockSize int `json:"logical_block_size"`
PhysicalBlockSize int `json:"physical_block_size"`
RotationRate int `json:"rotation_rate"`
FormFactor struct {
AtaValue int `json:"ata_value"`
Name string `json:"name"`
} `json:"form_factor"`
InSmartctlDatabase bool `json:"in_smartctl_database"`
AtaVersion struct {
String string `json:"string"`
MajorValue int `json:"major_value"`
MinorValue int `json:"minor_value"`
} `json:"ata_version"`
SataVersion struct {
String string `json:"string"`
Value int `json:"value"`
} `json:"sata_version"`
InterfaceSpeed struct {
Max struct {
SataValue int `json:"sata_value"`
String string `json:"string"`
UnitsPerSecond int `json:"units_per_second"`
BitsPerUnit int `json:"bits_per_unit"`
} `json:"max"`
Current struct {
SataValue int `json:"sata_value"`
String string `json:"string"`
UnitsPerSecond int `json:"units_per_second"`
BitsPerUnit int `json:"bits_per_unit"`
} `json:"current"`
} `json:"interface_speed"`
LocalTime struct {
TimeT int64 `json:"time_t"`
Asctime string `json:"asctime"`
} `json:"local_time"`
SmartStatus struct {
Passed bool `json:"passed"`
} `json:"smart_status"`
AtaSmartData struct {
OfflineDataCollection struct {
Status struct {
Value int `json:"value"`
String string `json:"string"`
Passed bool `json:"passed"`
} `json:"status"`
CompletionSeconds int `json:"completion_seconds"`
} `json:"offline_data_collection"`
SelfTest struct {
Status struct {
Value int `json:"value"`
String string `json:"string"`
RemainingPercent int `json:"remaining_percent"`
} `json:"status"`
PollingMinutes struct {
Short int `json:"short"`
Extended int `json:"extended"`
} `json:"polling_minutes"`
} `json:"self_test"`
Capabilities struct {
Values []int `json:"values"`
ExecOfflineImmediateSupported bool `json:"exec_offline_immediate_supported"`
OfflineIsAbortedUponNewCmd bool `json:"offline_is_aborted_upon_new_cmd"`
OfflineSurfaceScanSupported bool `json:"offline_surface_scan_supported"`
SelfTestsSupported bool `json:"self_tests_supported"`
ConveyanceSelfTestSupported bool `json:"conveyance_self_test_supported"`
SelectiveSelfTestSupported bool `json:"selective_self_test_supported"`
AttributeAutosaveEnabled bool `json:"attribute_autosave_enabled"`
ErrorLoggingSupported bool `json:"error_logging_supported"`
GpLoggingSupported bool `json:"gp_logging_supported"`
} `json:"capabilities"`
} `json:"ata_smart_data"`
AtaSctCapabilities struct {
Value int `json:"value"`
ErrorRecoveryControlSupported bool `json:"error_recovery_control_supported"`
FeatureControlSupported bool `json:"feature_control_supported"`
DataTableSupported bool `json:"data_table_supported"`
} `json:"ata_sct_capabilities"`
AtaSmartAttributes struct {
Revision int `json:"revision"`
Table []struct {
ID int `json:"id"`
Name string `json:"name"`
Value int `json:"value"`
Worst int `json:"worst"`
Thresh int `json:"thresh"`
WhenFailed string `json:"when_failed"`
Flags struct {
Value int `json:"value"`
String string `json:"string"`
Prefailure bool `json:"prefailure"`
UpdatedOnline bool `json:"updated_online"`
Performance bool `json:"performance"`
ErrorRate bool `json:"error_rate"`
EventCount bool `json:"event_count"`
AutoKeep bool `json:"auto_keep"`
} `json:"flags"`
Raw struct {
Value int64 `json:"value"`
String string `json:"string"`
} `json:"raw"`
} `json:"table"`
} `json:"ata_smart_attributes"`
PowerOnTime struct {
Hours int64 `json:"hours"`
} `json:"power_on_time"`
PowerCycleCount int64 `json:"power_cycle_count"`
Temperature struct {
Current int64 `json:"current"`
} `json:"temperature"`
AtaSmartErrorLog struct {
Summary struct {
Revision int `json:"revision"`
Count int `json:"count"`
} `json:"summary"`
} `json:"ata_smart_error_log"`
AtaSmartSelfTestLog struct {
Standard struct {
Revision int `json:"revision"`
Table []struct {
Type struct {
Value int `json:"value"`
String string `json:"string"`
} `json:"type"`
Status struct {
Value int `json:"value"`
String string `json:"string"`
Passed bool `json:"passed"`
} `json:"status"`
LifetimeHours int `json:"lifetime_hours"`
} `json:"table"`
Count int `json:"count"`
ErrorCountTotal int `json:"error_count_total"`
ErrorCountOutdated int `json:"error_count_outdated"`
} `json:"standard"`
} `json:"ata_smart_self_test_log"`
AtaSmartSelectiveSelfTestLog struct {
Revision int `json:"revision"`
Table []struct {
LbaMin int `json:"lba_min"`
LbaMax int `json:"lba_max"`
Status struct {
Value int `json:"value"`
String string `json:"string"`
} `json:"status"`
} `json:"table"`
Flags struct {
Value int `json:"value"`
RemainderScanEnabled bool `json:"remainder_scan_enabled"`
} `json:"flags"`
PowerUpScanResumeMinutes int `json:"power_up_scan_resume_minutes"`
} `json:"ata_smart_selective_self_test_log"`
}
+110
View File
@@ -0,0 +1,110 @@
package db
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"strings"
"time"
)
type DeviceRespWrapper struct {
Success bool `json:"success"`
Errors []error `json:"errors"`
Data []Device `json:"data"`
}
type Device struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
WWN string `json:"wwn" gorm:"primary_key"`
DeviceName string `json:"device_name"`
Manufacturer string `json:"manufacturer"`
ModelName string `json:"model_name"`
InterfaceType string `json:"interface_type"`
InterfaceSpeed string `json:"interface_speed"`
SerialNumber string `json:"serial_number"`
Firmware string `json:"firmware"`
RotationSpeed int `json:"rotational_speed"`
Capacity int64 `json:"capacity"`
FormFactor string `json:"form_factor"`
SmartSupport bool `json:"smart_support"`
SmartResults []Smart `gorm:"foreignkey:DeviceWWN" json:"smart_results"`
}
//This method requires a device with an array of SmartResults.
//It will remove all SmartResults other than the first (the latest one)
//All removed SmartResults, will be processed, grouping SmartAttribute by attribute_id
// and adding theme to an array called History.
func (dv *Device) SquashHistory() error {
if len(dv.SmartResults) <= 1 {
return nil //no history found. ignore
}
latestSmartResultSlice := dv.SmartResults[0:1]
historicalSmartResultSlice := dv.SmartResults[1:]
//re-assign the latest slice to the SmartResults field
dv.SmartResults = latestSmartResultSlice
//process the historical slice
history := map[int][]SmartAttribute{}
for _, smartResult := range historicalSmartResultSlice {
for _, smartAttribute := range smartResult.SmartAttributes {
if _, ok := history[smartAttribute.AttributeId]; !ok {
history[smartAttribute.AttributeId] = []SmartAttribute{}
}
history[smartAttribute.AttributeId] = append(history[smartAttribute.AttributeId], smartAttribute)
}
}
//now assign the historical slices to the SmartAttributes in the latest SmartResults
for sandx, smartAttribute := range dv.SmartResults[0].SmartAttributes {
if attributeHistory, ok := history[smartAttribute.AttributeId]; ok {
dv.SmartResults[0].SmartAttributes[sandx].History = attributeHistory
}
}
return nil
}
func (dv *Device) ApplyMetadataRules() error {
//embed metadata in the latest smart attributes object
if len(dv.SmartResults) > 0 {
for ndx, attr := range dv.SmartResults[0].SmartAttributes {
if strings.ToUpper(attr.WhenFailed) == SmartWhenFailedFailingNow {
//this attribute has previously failed
dv.SmartResults[0].SmartAttributes[ndx].Status = SmartAttributeStatusFailed
dv.SmartResults[0].SmartAttributes[ndx].StatusReason = "Attribute is failing manufacturer SMART threshold"
} else if strings.ToUpper(attr.WhenFailed) == SmartWhenFailedInThePast {
dv.SmartResults[0].SmartAttributes[ndx].Status = SmartAttributeStatusWarning
dv.SmartResults[0].SmartAttributes[ndx].StatusReason = "Attribute has previously failed manufacturer SMART threshold"
}
if smartMetadata, ok := metadata.AtaSmartAttributes[attr.AttributeId]; ok {
dv.SmartResults[0].SmartAttributes[ndx].MetadataObservedThresholdStatus(smartMetadata)
}
//check if status is blank, set to "passed"
if len(dv.SmartResults[0].SmartAttributes[ndx].Status) == 0 {
dv.SmartResults[0].SmartAttributes[ndx].Status = SmartAttributeStatusPassed
}
}
}
return nil
}
func (dv *Device) UpdateFromCollectorSmartInfo(info collector.SmartInfo) error {
dv.InterfaceSpeed = info.InterfaceSpeed.Current.String
dv.Firmware = info.FirmwareVersion
dv.RotationSpeed = info.RotationRate
dv.Capacity = info.UserCapacity.Bytes
dv.FormFactor = info.FormFactor.Name
//dv.SmartSupport =
return nil
}
+15
View File
@@ -0,0 +1,15 @@
package db
import "time"
type SelfTest struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
DeviceWWN string
Device Device `json:"-" gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
Date time.Time
}
+150
View File
@@ -0,0 +1,150 @@
package db
import (
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/jinzhu/gorm"
"time"
)
const SmartWhenFailedFailingNow = "FAILING_NOW"
const SmartWhenFailedInThePast = "IN_THE_PAST"
type Smart struct {
gorm.Model
DeviceWWN string `json:"device_wwn"`
Device Device `json:"-" gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
TestDate time.Time `json:"date"`
SmartStatus string `json:"smart_status"`
//Metrics
Temp int64 `json:"temp"`
PowerOnHours int64 `json:"power_on_hours"`
PowerCycleCount int64 `json:"power_cycle_count"`
SmartAttributes []SmartAttribute `json:"smart_attributes" gorm:"foreignkey:SmartId"`
}
func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) error {
sm.DeviceWWN = wwn
sm.TestDate = time.Unix(info.LocalTime.TimeT, 0)
//smart metrics
sm.Temp = info.Temperature.Current
sm.PowerCycleCount = info.PowerCycleCount
sm.PowerOnHours = info.PowerOnTime.Hours
sm.SmartAttributes = []SmartAttribute{}
for _, collectorAttr := range info.AtaSmartAttributes.Table {
attrModel := SmartAttribute{
AttributeId: collectorAttr.ID,
Name: collectorAttr.Name,
Value: collectorAttr.Value,
Worst: collectorAttr.Worst,
Threshold: collectorAttr.Thresh,
RawValue: collectorAttr.Raw.Value,
RawString: collectorAttr.Raw.String,
WhenFailed: collectorAttr.WhenFailed,
}
//now that we've parsed the data from the smartctl response, lets match it against our metadata rules and add additional Scrutiny specific data.
if smartMetadata, ok := metadata.AtaSmartAttributes[collectorAttr.ID]; ok {
attrModel.Name = smartMetadata.DisplayName
if smartMetadata.Transform != nil {
attrModel.TransformedValue = smartMetadata.Transform(attrModel.Value, attrModel.RawValue, attrModel.RawString)
}
}
sm.SmartAttributes = append(sm.SmartAttributes, attrModel)
}
if info.SmartStatus.Passed {
sm.SmartStatus = "passed"
} else {
sm.SmartStatus = "failed"
}
return nil
}
const SmartAttributeStatusPassed = "passed"
const SmartAttributeStatusFailed = "failed"
const SmartAttributeStatusWarning = "warn"
type SmartAttribute struct {
gorm.Model
SmartId int `json:"smart_id"`
Smart Device `json:"-" gorm:"foreignkey:SmartId"` // use SmartId as foreign key
AttributeId int `json:"attribute_id"`
Name string `json:"name"`
Value int `json:"value"`
Worst int `json:"worst"`
Threshold int `json:"thresh"`
RawValue int64 `json:"raw_value"`
RawString string `json:"raw_string"`
WhenFailed string `json:"when_failed"`
TransformedValue int64 `json:"transformed_value"`
Status string `gorm:"-" json:"status,omitempty"`
StatusReason string `gorm:"-" json:"status_reason,omitempty"`
FailureRate float64 `gorm:"-" json:"failure_rate,omitempty"`
History []SmartAttribute `gorm:"-" json:"history,omitempty"`
}
// compare the attribute (raw, normalized, transformed) value to observed thresholds, and update status if necessary
func (sa *SmartAttribute) MetadataObservedThresholdStatus(smartMetadata metadata.AtaSmartAttribute) {
//TODO: multiple rules
// try to predict the failure rates for observed thresholds that have 0 failure rate and error bars.
// - if the attribute is critical
// - the failure rate is over 10 - set to failed
// - the attribute does not match any threshold, set to warn
// - if the attribute is not critical
// - if failure rate is above 20 - set to failed
// - if failure rate is above 10 but below 20 - set to warn
//update the smart attribute status based on Observed thresholds.
var value int64
if smartMetadata.DisplayType == metadata.AtaSmartAttributeDisplayTypeNormalized {
value = int64(sa.Value)
} else if smartMetadata.DisplayType == metadata.AtaSmartAttributeDisplayTypeTransformed {
value = sa.TransformedValue
} else {
value = sa.RawValue
}
for _, obsThresh := range smartMetadata.ObservedThresholds {
//check if "value" is in this bucket
if ((obsThresh.Low == obsThresh.High) && value == obsThresh.Low) ||
(obsThresh.Low < value && value <= obsThresh.High) {
sa.FailureRate = obsThresh.AnnualFailureRate
if smartMetadata.Critical {
if obsThresh.AnnualFailureRate >= 0.10 {
sa.Status = SmartAttributeStatusFailed
sa.StatusReason = "Observed Failure Rate for Critical Attribute is greater than 10%"
}
} else {
if obsThresh.AnnualFailureRate >= 0.20 {
sa.Status = SmartAttributeStatusFailed
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 20%"
} else if obsThresh.AnnualFailureRate >= 0.10 {
sa.Status = SmartAttributeStatusWarning
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 10%"
}
}
//we've found the correct bucket, we can drop out of this loop
return
}
}
// no bucket found
if smartMetadata.Critical {
sa.Status = SmartAttributeStatusWarning
sa.StatusReason = "Could not determine Observed Failure Rate for Critical Attribute"
}
return
}
@@ -0,0 +1,42 @@
package db_test
import (
"encoding/json"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
"github.com/stretchr/testify/require"
"io/ioutil"
"os"
"testing"
)
func TestFromCollectorSmartInfo(t *testing.T) {
//setup
smartDataFile, err := os.Open("../testdata/smart.json")
require.NoError(t, err)
defer smartDataFile.Close()
var smartJson collector.SmartInfo
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
require.NoError(t, err)
err = json.Unmarshal(smartDataBytes, &smartJson)
require.NoError(t, err)
//test
smartMdl := db.Smart{}
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
//assert
require.NoError(t, err)
require.Equal(t, smartMdl.DeviceWWN, "WWN-test")
require.Equal(t, smartMdl.SmartStatus, "PASSED")
//check that temperature was correctly parsed
for _, attr := range smartMdl.SmartAttributes {
if attr.AttributeId == 194 {
require.Equal(t, int64(163210330144), attr.RawValue)
require.Equal(t, int64(32), attr.TransformedValue)
}
}
}
+846
View File
@@ -0,0 +1,846 @@
{
"json_format_version": [
1,
0
],
"smartctl": {
"version": [
7,
0
],
"svn_revision": "4883",
"platform_info": "x86_64-linux-4.19.128-flatcar",
"build_info": "(local build)",
"argv": [
"smartctl",
"-j",
"-a",
"/dev/sdb"
],
"exit_status": 0
},
"device": {
"name": "/dev/sdb",
"info_name": "/dev/sdb [SAT]",
"type": "sat",
"protocol": "ATA"
},
"model_name": "WDC WD140EDFZ-11A0VA0",
"serial_number": "9RK1XXXX",
"wwn": {
"naa": 5,
"oui": 3274,
"id": 10283057623
},
"firmware_version": "81.00A81",
"user_capacity": {
"blocks": 27344764928,
"bytes": 14000519643136
},
"logical_block_size": 512,
"physical_block_size": 4096,
"rotation_rate": 5400,
"form_factor": {
"ata_value": 2,
"name": "3.5 inches"
},
"in_smartctl_database": false,
"ata_version": {
"string": "ACS-2, ATA8-ACS T13/1699-D revision 4",
"major_value": 1020,
"minor_value": 41
},
"sata_version": {
"string": "SATA 3.2",
"value": 255
},
"interface_speed": {
"max": {
"sata_value": 14,
"string": "6.0 Gb/s",
"units_per_second": 60,
"bits_per_unit": 100000000
},
"current": {
"sata_value": 3,
"string": "6.0 Gb/s",
"units_per_second": 60,
"bits_per_unit": 100000000
}
},
"local_time": {
"time_t": 1592697810,
"asctime": "Sun Jun 21 00:03:30 2020 UTC"
},
"smart_status": {
"passed": true
},
"ata_smart_data": {
"offline_data_collection": {
"status": {
"value": 130,
"string": "was completed without error",
"passed": true
},
"completion_seconds": 101
},
"self_test": {
"status": {
"value": 241,
"string": "in progress, 10% remaining",
"remaining_percent": 10
},
"polling_minutes": {
"short": 2,
"extended": 1479
}
},
"capabilities": {
"values": [
91,
3
],
"exec_offline_immediate_supported": true,
"offline_is_aborted_upon_new_cmd": false,
"offline_surface_scan_supported": true,
"self_tests_supported": true,
"conveyance_self_test_supported": false,
"selective_self_test_supported": true,
"attribute_autosave_enabled": true,
"error_logging_supported": true,
"gp_logging_supported": true
}
},
"ata_sct_capabilities": {
"value": 61,
"error_recovery_control_supported": true,
"feature_control_supported": true,
"data_table_supported": true
},
"ata_smart_attributes": {
"revision": 16,
"table": [
{
"id": 1,
"name": "Raw_Read_Error_Rate",
"value": 100,
"worst": 100,
"thresh": 1,
"when_failed": "",
"flags": {
"value": 11,
"string": "PO-R-- ",
"prefailure": true,
"updated_online": true,
"performance": false,
"error_rate": true,
"event_count": false,
"auto_keep": false
},
"raw": {
"value": 0,
"string": "0"
}
},
{
"id": 2,
"name": "Throughput_Performance",
"value": 135,
"worst": 135,
"thresh": 54,
"when_failed": "",
"flags": {
"value": 4,
"string": "--S--- ",
"prefailure": false,
"updated_online": false,
"performance": true,
"error_rate": false,
"event_count": false,
"auto_keep": false
},
"raw": {
"value": 108,
"string": "108"
}
},
{
"id": 3,
"name": "Spin_Up_Time",
"value": 81,
"worst": 81,
"thresh": 1,
"when_failed": "",
"flags": {
"value": 7,
"string": "POS--- ",
"prefailure": true,
"updated_online": true,
"performance": true,
"error_rate": false,
"event_count": false,
"auto_keep": false
},
"raw": {
"value": 30089675132,
"string": "380 (Average 380)"
}
},
{
"id": 4,
"name": "Start_Stop_Count",
"value": 100,
"worst": 100,
"thresh": 0,
"when_failed": "",
"flags": {
"value": 18,
"string": "-O--C- ",
"prefailure": false,
"updated_online": true,
"performance": false,
"error_rate": false,
"event_count": true,
"auto_keep": false
},
"raw": {
"value": 9,
"string": "9"
}
},
{
"id": 5,
"name": "Reallocated_Sector_Ct",
"value": 100,
"worst": 100,
"thresh": 1,
"when_failed": "",
"flags": {
"value": 51,
"string": "PO--CK ",
"prefailure": true,
"updated_online": true,
"performance": false,
"error_rate": false,
"event_count": true,
"auto_keep": true
},
"raw": {
"value": 0,
"string": "0"
}
},
{
"id": 7,
"name": "Seek_Error_Rate",
"value": 100,
"worst": 100,
"thresh": 1,
"when_failed": "",
"flags": {
"value": 10,
"string": "-O-R-- ",
"prefailure": false,
"updated_online": true,
"performance": false,
"error_rate": true,
"event_count": false,
"auto_keep": false
},
"raw": {
"value": 0,
"string": "0"
}
},
{
"id": 8,
"name": "Seek_Time_Performance",
"value": 133,
"worst": 133,
"thresh": 20,
"when_failed": "",
"flags": {
"value": 4,
"string": "--S--- ",
"prefailure": false,
"updated_online": false,
"performance": true,
"error_rate": false,
"event_count": false,
"auto_keep": false
},
"raw": {
"value": 18,
"string": "18"
}
},
{
"id": 9,
"name": "Power_On_Hours",
"value": 100,
"worst": 100,
"thresh": 0,
"when_failed": "",
"flags": {
"value": 18,
"string": "-O--C- ",
"prefailure": false,
"updated_online": true,
"performance": false,
"error_rate": false,
"event_count": true,
"auto_keep": false
},
"raw": {
"value": 1730,
"string": "1730"
}
},
{
"id": 10,
"name": "Spin_Retry_Count",
"value": 100,
"worst": 100,
"thresh": 1,
"when_failed": "",
"flags": {
"value": 18,
"string": "-O--C- ",
"prefailure": false,
"updated_online": true,
"performance": false,
"error_rate": false,
"event_count": true,
"auto_keep": false
},
"raw": {
"value": 0,
"string": "0"
}
},
{
"id": 12,
"name": "Power_Cycle_Count",
"value": 100,
"worst": 100,
"thresh": 0,
"when_failed": "",
"flags": {
"value": 50,
"string": "-O--CK ",
"prefailure": false,
"updated_online": true,
"performance": false,
"error_rate": false,
"event_count": true,
"auto_keep": true
},
"raw": {
"value": 9,
"string": "9"
}
},
{
"id": 22,
"name": "Unknown_Attribute",
"value": 100,
"worst": 100,
"thresh": 25,
"when_failed": "",
"flags": {
"value": 35,
"string": "PO---K ",
"prefailure": true,
"updated_online": true,
"performance": false,
"error_rate": false,
"event_count": false,
"auto_keep": true
},
"raw": {
"value": 100,
"string": "100"
}
},
{
"id": 192,
"name": "Power-Off_Retract_Count",
"value": 100,
"worst": 100,
"thresh": 0,
"when_failed": "",
"flags": {
"value": 50,
"string": "-O--CK ",
"prefailure": false,
"updated_online": true,
"performance": false,
"error_rate": false,
"event_count": true,
"auto_keep": true
},
"raw": {
"value": 329,
"string": "329"
}
},
{
"id": 193,
"name": "Load_Cycle_Count",
"value": 100,
"worst": 100,
"thresh": 0,
"when_failed": "",
"flags": {
"value": 18,
"string": "-O--C- ",
"prefailure": false,
"updated_online": true,
"performance": false,
"error_rate": false,
"event_count": true,
"auto_keep": false
},
"raw": {
"value": 329,
"string": "329"
}
},
{
"id": 194,
"name": "Temperature_Celsius",
"value": 51,
"worst": 51,
"thresh": 0,
"when_failed": "",
"flags": {
"value": 2,
"string": "-O---- ",
"prefailure": false,
"updated_online": true,
"performance": false,
"error_rate": false,
"event_count": false,
"auto_keep": false
},
"raw": {
"value": 163210330144,
"string": "32 (Min/Max 24/38)"
}
},
{
"id": 196,
"name": "Reallocated_Event_Count",
"value": 100,
"worst": 100,
"thresh": 0,
"when_failed": "",
"flags": {
"value": 50,
"string": "-O--CK ",
"prefailure": false,
"updated_online": true,
"performance": false,
"error_rate": false,
"event_count": true,
"auto_keep": true
},
"raw": {
"value": 0,
"string": "0"
}
},
{
"id": 197,
"name": "Current_Pending_Sector",
"value": 100,
"worst": 100,
"thresh": 0,
"when_failed": "",
"flags": {
"value": 34,
"string": "-O---K ",
"prefailure": false,
"updated_online": true,
"performance": false,
"error_rate": false,
"event_count": false,
"auto_keep": true
},
"raw": {
"value": 0,
"string": "0"
}
},
{
"id": 198,
"name": "Offline_Uncorrectable",
"value": 100,
"worst": 100,
"thresh": 0,
"when_failed": "",
"flags": {
"value": 8,
"string": "---R-- ",
"prefailure": false,
"updated_online": false,
"performance": false,
"error_rate": true,
"event_count": false,
"auto_keep": false
},
"raw": {
"value": 0,
"string": "0"
}
},
{
"id": 199,
"name": "UDMA_CRC_Error_Count",
"value": 100,
"worst": 100,
"thresh": 0,
"when_failed": "",
"flags": {
"value": 10,
"string": "-O-R-- ",
"prefailure": false,
"updated_online": true,
"performance": false,
"error_rate": true,
"event_count": false,
"auto_keep": false
},
"raw": {
"value": 0,
"string": "0"
}
}
]
},
"power_on_time": {
"hours": 1730
},
"power_cycle_count": 9,
"temperature": {
"current": 32
},
"ata_smart_error_log": {
"summary": {
"revision": 1,
"count": 0
}
},
"ata_smart_self_test_log": {
"standard": {
"revision": 1,
"table": [
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1708
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1684
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1661
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1636
},
{
"type": {
"value": 2,
"string": "Extended offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1624
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1541
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1517
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1493
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1469
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1445
},
{
"type": {
"value": 2,
"string": "Extended offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1439
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1373
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1349
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1325
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1301
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1277
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1253
},
{
"type": {
"value": 2,
"string": "Extended offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1252
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1205
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1181
},
{
"type": {
"value": 1,
"string": "Short offline"
},
"status": {
"value": 0,
"string": "Completed without error",
"passed": true
},
"lifetime_hours": 1157
}
],
"count": 21,
"error_count_total": 0,
"error_count_outdated": 0
}
},
"ata_smart_selective_self_test_log": {
"revision": 1,
"table": [
{
"lba_min": 0,
"lba_max": 0,
"status": {
"value": 241,
"string": "Not_testing"
}
},
{
"lba_min": 0,
"lba_max": 0,
"status": {
"value": 241,
"string": "Not_testing"
}
},
{
"lba_min": 0,
"lba_max": 0,
"status": {
"value": 241,
"string": "Not_testing"
}
},
{
"lba_min": 0,
"lba_max": 0,
"status": {
"value": 241,
"string": "Not_testing"
}
},
{
"lba_min": 0,
"lba_max": 0,
"status": {
"value": 241,
"string": "Not_testing"
}
}
],
"flags": {
"value": 0,
"remainder_scan_enabled": false
},
"power_up_scan_resume_minutes": 0
}
}
+5
View File
@@ -0,0 +1,5 @@
package version
// VERSION is the app-global version string, which will be replaced with a
// new value during packaging
const VERSION = "1.0.0"
+8
View File
@@ -0,0 +1,8 @@
package web
// the following cronjobs need to be defined here:
// - get storage information for all approved disks
// - run short test against approved disks
// - run long test against approved disks
// - get S.M.A.R.T. metrics from approved disks
// - clean up / resolution for time-series data in sqlite database.
+77
View File
@@ -0,0 +1,77 @@
package web
import (
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
"github.com/jaypipes/ghw"
)
func RetrieveStorageDevices() ([]db.Device, error) {
block, err := ghw.Block()
if err != nil {
fmt.Printf("Error getting block storage info: %v", err)
return nil, err
}
approvedDisks := []db.Device{}
for _, disk := range block.Disks {
//TODO: always allow if in approved list
fmt.Printf(" %v\n", disk)
// ignore optical drives and floppy disks
if disk.DriveType == ghw.DRIVE_TYPE_FDD || disk.DriveType == ghw.DRIVE_TYPE_ODD {
fmt.Printf(" => Ignore: Optical or floppy disk - (found %s)\n", disk.DriveType.String())
continue
}
// ignore removable disks
if disk.IsRemovable {
fmt.Printf(" => Ignore: Removable disk (%v)\n", disk.IsRemovable)
continue
}
// ignore virtual disks & mobile phone storage devices
if disk.StorageController == ghw.STORAGE_CONTROLLER_VIRTIO || disk.StorageController == ghw.STORAGE_CONTROLLER_MMC {
fmt.Printf(" => Ignore: Virtual/multi-media storage controller - (found %s)\n", disk.StorageController.String())
continue
}
// ignore NVMe devices (not currently supported) TBA
if disk.StorageController == ghw.STORAGE_CONTROLLER_NVME {
fmt.Printf(" => Ignore: NVMe storage controller - (found %s)\n", disk.StorageController.String())
continue
}
// Skip unknown storage controllers, not usually S.M.A.R.T compatible.
if disk.StorageController == ghw.STORAGE_CONTROLLER_UNKNOWN {
fmt.Printf(" => Ignore: Unknown storage controller - (found %s)\n", disk.StorageController.String())
continue
}
//TODO: remove if in excluded list
diskModel := db.Device{
WWN: disk.WWN,
Manufacturer: disk.Vendor,
ModelName: disk.Model,
InterfaceType: disk.StorageController.String(),
//InterfaceSpeed: string
SerialNumber: disk.SerialNumber,
Capacity: int64(disk.SizeBytes),
//Firmware string
//RotationSpeed int
DeviceName: disk.Name,
}
if len(diskModel.WWN) == 0 {
//(macOS and some other os's) do not provide a WWN, so we're going to fallback to
//diskname as identifier if WWN is not present
diskModel.WWN = disk.Name
}
approvedDisks = append(approvedDisks, diskModel)
}
return approvedDisks, nil
}
+167
View File
@@ -0,0 +1,167 @@
package web
import (
"fmt"
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
log "github.com/sirupsen/logrus"
"net/http"
)
type AppEngine struct {
Config config.Interface
}
func (ae *AppEngine) Start() error {
r := gin.Default()
r.Use(database.DatabaseHandler(ae.Config.GetString("web.database.location")))
api := r.Group("/api")
{
api.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
})
})
//TODO: notifications
api.GET("/devices", GetDevicesHandler)
api.GET("/summary", GetDevicesSummary)
api.POST("/device/:wwn/smart", UploadDeviceSmartData)
api.POST("/device/:wwn/selftest", UploadDeviceSelfTestData)
api.GET("/device/:wwn/details", GetDeviceDetails)
}
//Static request routing
r.StaticFS("/web", http.Dir(ae.Config.GetString("web.src.frontend.path")))
//redirect base url to /web
r.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusFound, "/web")
})
//catch-all, serve index page.
r.NoRoute(func(c *gin.Context) {
c.File(fmt.Sprintf("%s/index.html", ae.Config.GetString("web.src.frontend.path")))
})
return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port"))) // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
// Get all active disks for processing by collectors
func GetDevicesHandler(c *gin.Context) {
storageDevices, err := RetrieveStorageDevices()
db := c.MustGet("DB").(*gorm.DB)
for _, dev := range storageDevices {
//insert devices into DB if not already there.
db.Where(dbModels.Device{WWN: dev.WWN}).FirstOrCreate(&dev)
}
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
})
} else {
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": storageDevices,
})
}
}
func UploadDeviceSmartData(c *gin.Context) {
db := c.MustGet("DB").(*gorm.DB)
var collectorSmartData collector.SmartInfo
err := c.BindJSON(&collectorSmartData)
if err != nil {
//TODO: cannot parse smart data
log.Error("Cannot parse SMART data")
c.JSON(http.StatusOK, gin.H{"success": false})
}
//update the device information if necessary
var device dbModels.Device
db.Where("wwn = ?", c.Param("wwn")).First(&device)
device.UpdateFromCollectorSmartInfo(collectorSmartData)
db.Model(&device).Updates(device)
// insert smart info
deviceSmartData := dbModels.Smart{}
err = deviceSmartData.FromCollectorSmartInfo(c.Param("wwn"), collectorSmartData)
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false})
return
}
db.Create(&deviceSmartData)
c.JSON(http.StatusOK, gin.H{"success": true})
}
func UploadDeviceSelfTestData(c *gin.Context) {
}
func GetDeviceDetails(c *gin.Context) {
db := c.MustGet("DB").(*gorm.DB)
device := dbModels.Device{}
db.Debug().
Preload("SmartResults", func(db *gorm.DB) *gorm.DB {
return db.Order("smarts.created_at DESC").Limit(40)
}).
Preload("SmartResults.SmartAttributes").
Where("wwn = ?", c.Param("wwn")).
First(&device)
device.SquashHistory()
device.ApplyMetadataRules()
c.JSON(http.StatusOK, gin.H{"success": true, "data": device, "lookup": metadata.AtaSmartAttributes})
}
func GetDevicesSummary(c *gin.Context) {
db := c.MustGet("DB").(*gorm.DB)
devices := []dbModels.Device{}
//OLD: cant seem to figure out how to get the latest SmartResults for each Device, so instead
// we're going to assume that results were retrieved at the same time, so we'll just get the last x number of results
//var devicesCount int
//db.Table("devices").Count(&devicesCount)
//We need the last x (for now all) Smart objects for each Device, so that we can graph Temperature
//We also need the last
db.Debug().
Preload("SmartResults", func(db *gorm.DB) *gorm.DB {
return db.Order("smarts.created_at DESC") //OLD: .Limit(devicesCount)
}).
//Preload("SmartResults").
// Preload("SmartResults.SmartAttributes").
Find(&devices)
//for _, dev := range devices {
// log.Printf("===== device: %s\n", dev.WWN)
// log.Print(len(dev.SmartResults))
//}
//a, _ := json.Marshal(devices) //get json byte array
//n := len(a) //Find the length of the byte array
//s := string(a[:n]) //convert to string
//log.Print(s) //write to response
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": devices,
})
//c.Data(http.StatusOK, "application/json", a)
}
+13
View File
@@ -0,0 +1,13 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false
+48
View File
@@ -0,0 +1,48 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
/dist
+70
View File
@@ -0,0 +1,70 @@
// -----------------------------------------------------------------------------------------------------
// @ 3rd party credits
// -----------------------------------------------------------------------------------------------------
// Icons
Material - https://material.io/tools/icons
Dripicons - http://demo.amitjakhu.com/dripicons/
Feather - https://feathericons.com/
Heroicons - https://github.com/refactoringui/heroicons
Iconsmind - https://iconsmind.com/
// Avatars
https://uifaces.co/
// Mail app
Photo by Riccardo Chiarini on Unsplash - https://unsplash.com/photos/2VDa8bnLM8c
Photo by Johannes Plenio on Unsplash - https://unsplash.com/photos/RwHv7LgeC7s
Photo by Jamie Davies on Unsplash - https://unsplash.com/photos/Hao52Fu9-F8
Photo by Christian Joudrey on Unsplash - https://unsplash.com/photos/mWRR1xj95hg
// Auth pages
Photo by Meric Dagli on Unsplash - https://unsplash.com/photos/kZTYGpoeQO0
// Profile page
Photo by Alex Knight on Unsplash - https://unsplash.com/photos/DpPutJwgyW8
// Cards
Photo by Kym Ellis on Unsplash - https://unsplash.com/photos/RPT3AjdXlZc
Photo by Patrick Hendry on Unsplash - https://unsplash.com/photos/Qgxk3PQsMiI
Photo by Hailey Kean on Unsplash - https://unsplash.com/photos/QxjsOlFNr_4
Photo by Nathan Anderson on Unsplash - https://unsplash.com/photos/mG8ShlWrMDI
Photo by Adrian Infernus on Unsplash - https://unsplash.com/photos/5apewqWk978
Photo by freestocks.org on Unsplash - https://unsplash.com/photos/c73TZ2sIU38
Photo by Tim Marshall on Unsplash - https://unsplash.com/photos/PKSCrmZdvwA
Photo by Daniel Koponyas on Unsplash - https://unsplash.com/photos/rbiLY6ZwvXQ
Photo by John Westrock on Unsplash - https://unsplash.com/photos/LCesauDseu8
Photo by Gabriel Sollmann on Unsplash - https://unsplash.com/photos/kFWj9y-tJB4
Photo by Kevin Wolf on Unsplash - https://unsplash.com/photos/BJyjgEdNTPs
Photo by Luca Bravo on Unsplash - https://unsplash.com/photos/hFzIoD0F_i8
Photo by Ian Baldwin on Unsplash - https://unsplash.com/photos/Dlj-SxxTlQ0
Photo by Ben Kolde on Unsplash - https://unsplash.com/photos/KRTFIBOfcFw
Photo by Chad Peltola on Unsplash - https://unsplash.com/photos/BTvQ2ET_iKc
Photo by rocknwool on Unsplash - https://unsplash.com/photos/r56oO1V5oms
Photo by Vita Vilcina on Unsplash - https://unsplash.com/photos/KtOid0FLjqU
Photo by Jia Ye on Unsplash - https://unsplash.com/photos/y8ZnQqgohLk
Photo by Parker Whitson on Unsplash - https://unsplash.com/photos/OlTYIqTjmVM
Photo by Dorian Hurst on Unsplash - https://unsplash.com/photos/a9uWPQlIbYc
Photo by Everaldo Coelho on Unsplash - https://unsplash.com/photos/KPaSCpklCZw
Photo by eberhard grossgasteiger on Unsplash - https://unsplash.com/photos/fh2JefbNlII
Photo by Orlova Maria on Unsplash - https://unsplash.com/photos/p8y4dWEMGMU
Photo by Jake Blucker on Unsplash - https://unsplash.com/photos/tMzCrBkM99Y
Photo by Jerry Zhang on Unsplash - https://unsplash.com/photos/oIBcow6n36s
Photo by John Cobb on Unsplash - https://unsplash.com/photos/IE_sifhay7o
Photo by Dan Gold on Unsplash - https://unsplash.com/photos/mDlhOIfGxNI
Photo by Ana Toma on Unsplash - https://unsplash.com/photos/XsGwe6gYg0c
Photo by Andrea on Unsplash - https://unsplash.com/photos/1AWY0N960Sk
Photo by Aswin on Unsplash - https://unsplash.com/photos/_roUcFWstas
Photo by Justin Kauffman on Unsplash - https://unsplash.com/photos/aWG_dqyhI0A
Photo by Barna Bartis on Unsplash - https://unsplash.com/photos/VVoBQqWrvkc
Photo by Kyle Hinkson on Unsplash - https://unsplash.com/photos/3439EnvnAGo
Photo by Spencer Watson on Unsplash - https://unsplash.com/photos/5TBf16GnHKg
Photo by adrian on Unsplash - https://unsplash.com/photos/1wrzvwoK8A4
Photo by Christopher Rusev on Unsplash - https://unsplash.com/photos/7gKWgCRixf0
Photo by Stephen Leonardi on Unsplash - https://unsplash.com/photos/MDmwQVgDHHM
Photo by Dwinanda Nurhanif Mujito on Unsplash - https://unsplash.com/photos/pKT5Mg16w_w
Photo by Humphrey Muleba on Unsplash - https://unsplash.com/photos/Zuvf5mxT5fs
Photo by adrian on Unsplash - https://unsplash.com/photos/PNRxLFPMyJY
Photo by Dahee Son on Unsplash - https://unsplash.com/photos/tV06QVJXVxU
Photo by Zachary Kyra-Derksen on Unsplash - https://unsplash.com/photos/vkqS7vLQUtg
Photo by Rodrigo Soares on Unsplash - https://unsplash.com/photos/8BFWBUkSqQo
+6
View File
@@ -0,0 +1,6 @@
Envato Standard License
Copyright (c) Sercan Yemen <sercanyemen@gmail.com>
This project is protected by Envato's Standard License. For more information,
check the official license page at [https://themeforest.net/licenses/standard](https://themeforest.net/licenses/standard)
+25
View File
@@ -0,0 +1,25 @@
# Treo - Admin template and Starter project for Angular
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
+139
View File
@@ -0,0 +1,139 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"treo": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/treo",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon-16x16.png",
"src/favicon-32x32.png",
"src/assets"
],
"stylePreprocessorOptions": {
"includePaths": [
"src/@treo/styles"
]
},
"styles": [
"src/styles/vendors.scss",
"src/@treo/styles/main.scss",
"src/styles/styles.scss",
"src/styles/tailwind.scss"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "5mb",
"maximumError": "8mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "60kb",
"maximumError": "100kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "treo:build"
},
"configurations": {
"production": {
"browserTarget": "treo:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "treo:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon-16x16.png",
"src/favicon-32x32.png",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "treo:serve"
},
"configurations": {
"production": {
"devServerTarget": "treo:serve:production"
}
}
}
}
}
},
"defaultProject": "treo"
}
+12
View File
@@ -0,0 +1,12 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.
+35
View File
@@ -0,0 +1,35 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const {SpecReporter} = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs : [
'./src/**/*.e2e-spec.ts'
],
capabilities : {
browserName: 'chrome'
},
directConnect : true,
baseUrl : 'http://localhost:4200/',
framework : 'jasmine',
jasmineNodeOpts : {
showColors : true,
defaultTimeoutInterval: 30000,
print : function ()
{
}
},
onPrepare()
{
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}}));
}
};
+23
View File
@@ -0,0 +1,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Welcome to Treo!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE
} as logging.Entry));
});
});
+14
View File
@@ -0,0 +1,14 @@
import { browser, by, element } from 'protractor';
export class AppPage
{
navigateTo(): Promise<unknown>
{
return browser.get(browser.baseUrl) as Promise<unknown>;
}
getTitleText(): Promise<string>
{
return element(by.css('app-root h1')).getText() as Promise<string>;
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}
+33
View File
@@ -0,0 +1,33 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config)
{
config.set({
basePath : '',
frameworks : ['jasmine', '@angular-devkit/build-angular'],
plugins : [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client : {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir : require('path').join(__dirname, './coverage/treo'),
reports : ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters : ['progress', 'kjhtml'],
port : 9876,
colors : true,
logLevel : config.LOG_INFO,
autoWatch : true,
browsers : ['Chrome'],
singleRun : false,
restartOnFileChange : true
});
};
+14972
View File
File diff suppressed because it is too large Load Diff
+83
View File
@@ -0,0 +1,83 @@
{
"name": "@treo/starter",
"version": "1.0.1",
"license": "https://themeforest.net/licenses/standard",
"scripts": {
"ng": "ng",
"start": "ng serve --open",
"start:mem": "node --max_old_space_size=6144 ./node_modules/@angular/cli/bin/ng serve --open",
"build": "ng build",
"build:prod": "ng build --prod",
"build:prod:mem": "node --max_old_space_size=6144 ./node_modules/@angular/cli/bin/ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"tw": "npm run tw:build && npm run tw:export",
"tw:build": "./node_modules/.bin/tailwind build src/tailwind/main.css -c src/tailwind/config.js -o src/styles/tailwind.scss",
"tw:export": "npm run tw:export:js && npm run tw:export:scss",
"tw:export:js": "node src/@treo/tailwind/export.js -c src/tailwind/config.js -o src/@treo/tailwind/exported/variables.ts",
"tw:export:scss": "./node_modules/.bin/tailwind build src/@treo/tailwind/export.css -c src/tailwind/config.js -o src/@treo/tailwind/exported/_variables.scss"
},
"private": true,
"dependencies": {
"@angular/animations": "9.1.4",
"@angular/cdk": "9.2.2",
"@angular/common": "9.1.4",
"@angular/compiler": "9.1.4",
"@angular/core": "9.1.4",
"@angular/forms": "9.1.4",
"@angular/material": "9.2.2",
"@angular/material-moment-adapter": "9.2.2",
"@angular/platform-browser": "9.1.4",
"@angular/platform-browser-dynamic": "9.1.4",
"@angular/router": "9.1.4",
"@fullcalendar/angular": "4.4.5-beta",
"@fullcalendar/core": "4.4.0",
"@fullcalendar/daygrid": "4.4.0",
"@fullcalendar/interaction": "4.4.0",
"@fullcalendar/list": "4.4.0",
"@fullcalendar/moment": "4.4.0",
"@fullcalendar/rrule": "4.4.0",
"@fullcalendar/timegrid": "4.4.0",
"apexcharts": "3.19.0",
"crypto-js": "3.3.0",
"highlight.js": "10.0.1",
"lodash": "4.17.15",
"moment": "2.24.0",
"ng-apexcharts": "1.2.3",
"ngx-markdown": "9.0.0",
"ngx-quill": "9.1.0",
"perfect-scrollbar": "1.5.0",
"quill": "1.3.7",
"rrule": "2.6.4",
"rxjs": "6.5.5",
"tslib": "1.11.1",
"web-animations-js": "2.3.2",
"zone.js": "0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "0.901.4",
"@angular/cli": "9.1.4",
"@angular/compiler-cli": "9.1.4",
"@angular/language-service": "9.1.4",
"@types/crypto-js": "3.1.45",
"@types/highlight.js": "9.12.3",
"@types/jasmine": "3.5.10",
"@types/jasminewd2": "2.0.8",
"@types/lodash": "4.14.150",
"@types/node": "12.12.37",
"codelyzer": "5.2.2",
"jasmine-core": "3.5.0",
"jasmine-spec-reporter": "4.2.1",
"karma": "5.0.4",
"karma-chrome-launcher": "3.1.0",
"karma-coverage-istanbul-reporter": "2.1.1",
"karma-jasmine": "3.0.3",
"karma-jasmine-html-reporter": "1.5.3",
"protractor": "5.4.4",
"tailwindcss": "1.4.4",
"ts-node": "8.3.0",
"tslint": "6.1.2",
"typescript": "3.8.3"
}
}
@@ -0,0 +1,14 @@
export class TreoAnimationCurves
{
static STANDARD_CURVE = 'cubic-bezier(0.4, 0.0, 0.2, 1)';
static DECELERATION_CURVE = 'cubic-bezier(0.0, 0.0, 0.2, 1)';
static ACCELERATION_CURVE = 'cubic-bezier(0.4, 0.0, 1, 1)';
static SHARP_CURVE = 'cubic-bezier(0.4, 0.0, 0.6, 1)';
}
export class TreoAnimationDurations
{
static COMPLEX = '375ms';
static ENTERING = '225ms';
static EXITING = '195ms';
}
@@ -0,0 +1,34 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
// -----------------------------------------------------------------------------------------------------
// @ Expand / collapse
// -----------------------------------------------------------------------------------------------------
const expandCollapse = trigger('expandCollapse',
[
state('void, collapsed',
style({
height: '0'
})
),
state('*, expanded',
style('*')
),
// Prevent the transition if the state is false
transition('void <=> false, collapsed <=> false, expanded <=> false', []),
// Transition
transition('void <=> *, collapsed <=> expanded',
animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
export { expandCollapse };
@@ -0,0 +1,330 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
// -----------------------------------------------------------------------------------------------------
// @ Fade in
// -----------------------------------------------------------------------------------------------------
const fadeIn = trigger('fadeIn',
[
state('void',
style({
opacity: 0
})
),
state('*',
style({
opacity: 1
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade in top
// -----------------------------------------------------------------------------------------------------
const fadeInTop = trigger('fadeInTop',
[
state('void',
style({
opacity : 0,
transform: 'translate3d(0, -100%, 0)'
})
),
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade in bottom
// -----------------------------------------------------------------------------------------------------
const fadeInBottom = trigger('fadeInBottom',
[
state('void',
style({
opacity : 0,
transform: 'translate3d(0, 100%, 0)'
})
),
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade in left
// -----------------------------------------------------------------------------------------------------
const fadeInLeft = trigger('fadeInLeft',
[
state('void',
style({
opacity : 0,
transform: 'translate3d(-100%, 0, 0)'
})
),
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade in right
// -----------------------------------------------------------------------------------------------------
const fadeInRight = trigger('fadeInRight',
[
state('void',
style({
opacity : 0,
transform: 'translate3d(100%, 0, 0)'
})
),
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade out
// -----------------------------------------------------------------------------------------------------
const fadeOut = trigger('fadeOut',
[
state('*',
style({
opacity: 1
})
),
state('void',
style({
opacity: 0
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade out top
// -----------------------------------------------------------------------------------------------------
const fadeOutTop = trigger('fadeOutTop',
[
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
opacity : 0,
transform: 'translate3d(0, -100%, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade out bottom
// -----------------------------------------------------------------------------------------------------
const fadeOutBottom = trigger('fadeOutBottom',
[
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
opacity : 0,
transform: 'translate3d(0, 100%, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade out left
// -----------------------------------------------------------------------------------------------------
const fadeOutLeft = trigger('fadeOutLeft',
[
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
opacity : 0,
transform: 'translate3d(-100%, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Fade out right
// -----------------------------------------------------------------------------------------------------
const fadeOutRight = trigger('fadeOutRight',
[
state('*',
style({
opacity : 1,
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
opacity : 0,
transform: 'translate3d(100%, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
export { fadeIn, fadeInTop, fadeInBottom, fadeInLeft, fadeInRight, fadeOut, fadeOutTop, fadeOutBottom, fadeOutLeft, fadeOutRight };
@@ -0,0 +1 @@
export * from './public-api';
@@ -0,0 +1,15 @@
import { expandCollapse } from './expand-collapse';
import { fadeIn, fadeInBottom, fadeInLeft, fadeInRight, fadeInTop, fadeOut, fadeOutBottom, fadeOutLeft, fadeOutRight, fadeOutTop } from './fade';
import { shake } from './shake';
import { slideInBottom, slideInLeft, slideInRight, slideInTop, slideOutBottom, slideOutLeft, slideOutRight, slideOutTop } from './slide';
import { zoomIn, zoomOut } from './zoom';
export const TreoAnimations = [
expandCollapse,
fadeIn, fadeInTop, fadeInBottom, fadeInLeft, fadeInRight,
fadeOut, fadeOutTop, fadeOutBottom, fadeOutLeft, fadeOutRight,
shake,
slideInTop, slideInBottom, slideInLeft, slideInRight,
slideOutTop, slideOutBottom, slideOutLeft, slideOutRight,
zoomIn, zoomOut
];
@@ -0,0 +1,73 @@
import { animate, keyframes, style, transition, trigger } from '@angular/animations';
// -----------------------------------------------------------------------------------------------------
// @ Shake
// -----------------------------------------------------------------------------------------------------
const shake = trigger('shake',
[
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *, * => true',
[
animate('{{timings}}',
keyframes([
style({
transform: 'translate3d(0, 0, 0)',
offset : 0
}),
style({
transform: 'translate3d(-10px, 0, 0)',
offset : 0.1
}),
style({
transform: 'translate3d(10px, 0, 0)',
offset : 0.2
}),
style({
transform: 'translate3d(-10px, 0, 0)',
offset : 0.3
}),
style({
transform: 'translate3d(10px, 0, 0)',
offset : 0.4
}),
style({
transform: 'translate3d(-10px, 0, 0)',
offset : 0.5
}),
style({
transform: 'translate3d(10px, 0, 0)',
offset : 0.6
}),
style({
transform: 'translate3d(-10px, 0, 0)',
offset : 0.7
}),
style({
transform: 'translate3d(10px, 0, 0)',
offset : 0.8
}),
style({
transform: 'translate3d(-10px, 0, 0)',
offset : 0.9
}),
style({
transform: 'translate3d(0, 0, 0)',
offset : 1
})
])
)
],
{
params: {
timings: '0.8s cubic-bezier(0.455, 0.03, 0.515, 0.955)'
}
}
)
]
);
export { shake };
@@ -0,0 +1,252 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
// -----------------------------------------------------------------------------------------------------
// @ Slide in top
// -----------------------------------------------------------------------------------------------------
const slideInTop = trigger('slideInTop',
[
state('void',
style({
transform: 'translate3d(0, -100%, 0)'
})
),
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide in bottom
// -----------------------------------------------------------------------------------------------------
const slideInBottom = trigger('slideInBottom',
[
state('void',
style({
transform: 'translate3d(0, 100%, 0)'
})
),
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide in left
// -----------------------------------------------------------------------------------------------------
const slideInLeft = trigger('slideInLeft',
[
state('void',
style({
transform: 'translate3d(-100%, 0, 0)'
})
),
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide in right
// -----------------------------------------------------------------------------------------------------
const slideInRight = trigger('slideInRight',
[
state('void',
style({
transform: 'translate3d(100%, 0, 0)'
})
),
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide out top
// -----------------------------------------------------------------------------------------------------
const slideOutTop = trigger('slideOutTop',
[
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
transform: 'translate3d(0, -100%, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide out bottom
// -----------------------------------------------------------------------------------------------------
const slideOutBottom = trigger('slideOutBottom',
[
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
transform: 'translate3d(0, 100%, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide out left
// -----------------------------------------------------------------------------------------------------
const slideOutLeft = trigger('slideOutLeft',
[
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
transform: 'translate3d(-100%, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Slide out right
// -----------------------------------------------------------------------------------------------------
const slideOutRight = trigger('slideOutRight',
[
state('*',
style({
transform: 'translate3d(0, 0, 0)'
})
),
state('void',
style({
transform: 'translate3d(100%, 0, 0)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
export { slideInTop, slideInBottom, slideInLeft, slideInRight, slideOutTop, slideOutBottom, slideOutLeft, slideOutRight };
@@ -0,0 +1,73 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
// -----------------------------------------------------------------------------------------------------
// @ Zoom in
// -----------------------------------------------------------------------------------------------------
const zoomIn = trigger('zoomIn',
[
state('void',
style({
opacity : 0,
transform: 'scale(0.5)'
})
),
state('*',
style({
opacity : 1,
transform: 'scale(1)'
})
),
// Prevent the transition if the state is false
transition('void => false', []),
// Transition
transition('void => *', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
}
}
)
]
);
// -----------------------------------------------------------------------------------------------------
// @ Zoom out
// -----------------------------------------------------------------------------------------------------
const zoomOut = trigger('zoomOut',
[
state('*',
style({
opacity : 1,
transform: 'scale(1)'
})
),
state('void',
style({
opacity : 0,
transform: 'scale(0.5)'
})
),
// Prevent the transition if the state is false
transition('false => void', []),
// Transition
transition('* => void', animate('{{timings}}'),
{
params: {
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
}
}
)
]
);
export { zoomIn, zoomOut };
@@ -0,0 +1,29 @@
<!-- Flippable card -->
<ng-container *ngIf="flippable">
<!-- Front -->
<div class="treo-card-front">
<ng-content select="[treoCardFront]"></ng-content>
</div>
<!-- Back -->
<div class="treo-card-back">
<ng-content select="[treoCardBack]"></ng-content>
</div>
</ng-container>
<!-- Normal card -->
<ng-container *ngIf="!flippable">
<!-- Content -->
<ng-content></ng-content>
<!-- Expansion -->
<div class="treo-card-expansion"
*ngIf="expanded"
[@expandCollapse]>
<ng-content select="[treoCardExpansion]"></ng-content>
</div>
</ng-container>
@@ -0,0 +1,85 @@
@import 'treo';
treo-card {
position: relative;
display: flex;
border-radius: 8px;
overflow: hidden;
@include treo-elevation('md');
// Flippable
&.treo-card-flippable {
border-radius: 0;
overflow: visible;
transform-style: preserve-3d;
transition: transform 1s;
@include treo-elevation('none');
&.treo-card-flipped {
.treo-card-front {
visibility: hidden;
opacity: 0;
transform: rotateY(180deg);
}
.treo-card-back {
visibility: visible;
opacity: 1;
transform: rotateY(360deg);
}
}
.treo-card-front,
.treo-card-back {
display: flex;
flex-direction: column;
flex: 1 1 auto;
z-index: 10;
border-radius: 8px;
transition: transform 0.5s ease-out 0s, visibility 0s ease-in 0.2s, opacity 0s ease-in 0.2s;
backface-visibility: hidden;
@include treo-elevation('md');
}
.treo-card-front {
position: relative;
opacity: 1;
visibility: visible;
transform: rotateY(0deg);
overflow: hidden;
}
.treo-card-back {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
visibility: hidden;
transform: rotateY(180deg);
overflow: hidden auto;
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
treo-card {
background: map-get($background, card);
&.treo-card-flippable {
background: transparent;
.treo-card-front,
.treo-card-back {
background: map-get($background, card);
}
}
}
}
@@ -0,0 +1,125 @@
import { Component, ElementRef, Input, Renderer2, ViewEncapsulation } from '@angular/core';
import { TreoAnimations } from '@treo/animations';
@Component({
selector : 'treo-card',
templateUrl : './card.component.html',
styleUrls : ['./card.component.scss'],
encapsulation: ViewEncapsulation.None,
animations : TreoAnimations,
exportAs : 'treoCard'
})
export class TreoCardComponent
{
expanded: boolean;
flipped: boolean;
// Private
private _flippable: boolean;
/**
* Constructor
*
* @param {Renderer2} _renderer2
* @param {ElementRef} _elementRef
*/
constructor(
private _renderer2: Renderer2,
private _elementRef: ElementRef
)
{
// Set the defaults
this.expanded = false;
this.flippable = false;
this.flipped = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for flippable
*
* @param value
*/
@Input()
set flippable(value: boolean)
{
// If the value is the same, return...
if ( this._flippable === value )
{
return;
}
// Update the class name
if ( value )
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-card-flippable');
}
else
{
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-card-flippable');
}
// Store the value
this._flippable = value;
}
get flippable(): boolean
{
return this._flippable;
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Expand the details
*/
expand(): void
{
this.expanded = true;
}
/**
* Collapse the details
*/
collapse(): void
{
this.expanded = false;
}
/**
* Toggle the expand/collapse status
*/
toggleExpanded(): void
{
this.expanded = !this.expanded;
}
/**
* Flip the card
*/
flip(): void
{
// Return if not flippable
if ( !this.flippable )
{
return;
}
this.flipped = !this.flipped;
// Update the class name
if ( this.flipped )
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-card-flipped');
}
else
{
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-card-flipped');
}
}
}
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TreoCardComponent } from '@treo/components/card/card.component';
@NgModule({
declarations: [
TreoCardComponent
],
imports : [
CommonModule
],
exports : [
TreoCardComponent
]
})
export class TreoCardModule
{
}
@@ -0,0 +1 @@
export * from '@treo/components/card/public-api';
@@ -0,0 +1,2 @@
export * from '@treo/components/card/card.component';
export * from '@treo/components/card/card.module';
@@ -0,0 +1,90 @@
<div class="range"
(click)="openPickerPanel()"
#pickerPanelOrigin>
<div class="start">
<div class="date">{{range.startDate}}</div>
<div class="time"
*ngIf="range.startTime">{{range.startTime}}</div>
</div>
<div class="separator">-</div>
<div class="end">
<div class="date">{{range.endDate}}</div>
<div class="time"
*ngIf="range.endTime">{{range.endTime}}</div>
</div>
</div>
<ng-template #pickerPanel>
<!-- Start -->
<div class="start">
<div class="month">
<div class="month-header">
<button class="previous-button"
mat-icon-button
(click)="prev()"
tabindex="1">
<mat-icon [svgIcon]="'chevron_left'"></mat-icon>
</button>
<div class="month-label">{{getMonthLabel(1)}}</div>
</div>
<mat-month-view [(activeDate)]="activeDates.month1"
[dateFilter]="dateFilter()"
[dateClass]="dateClass()"
(click)="$event.stopImmediatePropagation()"
(selectedChange)="onSelectedDateChange($event)"
#matMonthView1>
</mat-month-view>
</div>
<mat-form-field class="treo-mat-no-subscript time start-time"
*ngIf="timeRange">
<input matInput
[autocomplete]="'off'"
[formControl]="startTimeFormControl"
(blur)="updateStartTime($event)"
tabindex="3">
<mat-label>Start time</mat-label>
</mat-form-field>
</div>
<!-- End -->
<div class="end">
<div class="month">
<div class="month-header">
<div class="month-label">{{getMonthLabel(2)}}</div>
<button class="next-button"
mat-icon-button
(click)="next()"
tabindex="2">
<mat-icon [svgIcon]="'chevron_right'"></mat-icon>
</button>
</div>
<mat-month-view [(activeDate)]="activeDates.month2"
[dateFilter]="dateFilter()"
[dateClass]="dateClass()"
(click)="$event.stopImmediatePropagation()"
(selectedChange)="onSelectedDateChange($event)"
#matMonthView2>
</mat-month-view>
</div>
<mat-form-field class="treo-mat-no-subscript time end-time"
*ngIf="timeRange">
<input matInput
[formControl]="endTimeFormControl"
(blur)="updateEndTime($event)"
tabindex="4">
<mat-label>End time</mat-label>
</mat-form-field>
</div>
</ng-template>
@@ -0,0 +1,351 @@
@import 'treo';
// Variables
$body-cell-padding: 2px;
treo-date-range {
display: flex;
.range {
display: flex;
align-items: center;
height: 48px;
min-height: 48px;
max-height: 48px;
cursor: pointer;
.start,
.end {
display: flex;
align-items: center;
height: 100%;
padding: 0 16px;
border-radius: 5px;
border-width: 1px;
line-height: 1;
.date {
white-space: nowrap;
+ .time {
margin-left: 8px;
}
}
.time {
white-space: nowrap;
}
}
.separator {
margin: 0 12px;
@include treo-breakpoint('xs') {
margin: 0 2px;
}
}
}
}
.treo-date-range-panel {
border-radius: 4px;
padding: 24px;
.start,
.end {
display: flex;
flex-direction: column;
.month {
max-width: 196px;
min-width: 196px;
width: 196px;
.month-header {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 32px;
margin-bottom: 16px;
.previous-button,
.next-button {
position: absolute;
width: 24px !important;
height: 24px !important;
min-height: 24px !important;
max-height: 24px !important;
line-height: 24px !important;
.mat-icon {
@include treo-icon-size(20);
}
}
.previous-button {
left: 0;
}
.next-button {
right: 0;
}
.month-label {
font-weight: 500;
}
}
mat-month-view {
display: flex;
min-height: 188px;
.mat-calendar-table {
width: 100%;
border-collapse: collapse;
tbody {
tr {
&[aria-hidden=true] {
display: none !important;
}
&:first-child {
td:first-child {
&[aria-hidden=true] {
visibility: hidden;
pointer-events: none;
opacity: 0;
}
}
}
td.mat-calendar-body-cell {
width: 28px !important;
height: 28px !important;
padding: $body-cell-padding !important;
&.treo-date-range {
position: relative;
&:before {
content: '';
position: absolute;
top: $body-cell-padding;
right: 0;
bottom: $body-cell-padding;
left: 0;
}
&.treo-date-range-start {
&:before {
left: $body-cell-padding;
border-radius: 999px 0 0 999px;
}
&.treo-date-range-end,
&:last-child {
&:before {
right: $body-cell-padding;
border-radius: 999px;
}
}
}
&.treo-date-range-end {
&:before {
right: $body-cell-padding;
border-radius: 0 999px 999px 0;
}
&:first-child {
&:before {
left: $body-cell-padding;
border-radius: 999px;
}
}
}
&:first-child {
&:before {
border-radius: 999px 0 0 999px;
}
}
&:last-child {
&:before {
border-radius: 0 999px 999px 0;
}
}
}
.mat-calendar-body-cell-content {
position: relative;
top: 0;
left: 0;
width: 24px;
height: 24px;
font-size: 12px;
}
}
td.mat-calendar-body-label {
+ td.mat-calendar-body-cell {
&.treo-date-range {
&:before {
border-radius: 999px 0 0 999px;
}
&.treo-date-range-start {
&.treo-date-range-end {
border-radius: 999px;
}
}
&.treo-date-range-end {
&:before {
left: $body-cell-padding;
border-radius: 999px;
}
}
}
}
}
}
}
}
}
}
.time {
width: 100%;
max-width: 196px;
}
}
.start {
align-items: flex-start;
margin-right: 20px;
.month {
.month-label {
margin-left: 8px;
}
}
}
.end {
align-items: flex-end;
margin-left: 20px;
.month {
.month-label {
margin-right: 8px;
}
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$primary: map-get($theme, primary);
$is-dark: map-get($theme, is-dark);
treo-date-range {
.range {
.start,
.end {
@if ($is-dark) {
background-color: rgba(0, 0, 0, 0.05);
border-color: treo-color('cool-gray', 500);
} @else {
background-color: treo-color('cool-gray', 50);
border-color: treo-color('cool-gray', 300);
}
}
}
}
.treo-date-range-panel {
background: map-get($background, card);
@include treo-elevation('2xl');
.start,
.end {
.month {
.month-header {
.month-label {
color: map-get($foreground, secondary-text);
}
}
mat-month-view {
.mat-calendar-table {
tbody {
tr {
td,
td:hover {
&.treo-date-range {
&:before {
background-color: map-get($primary, 200);
}
.mat-calendar-body-cell-content {
background-color: transparent;
}
}
&.treo-date-range-start,
&.treo-date-range-end {
.mat-calendar-body-cell-content {
background-color: map-get($primary, default);
color: map-get($primary, default-contrast);
}
}
.mat-calendar-body-today {
border: none;
}
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,715 @@
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, HostBinding, Input, OnDestroy, OnInit, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Overlay } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { MatCalendarCellCssClasses, MatMonthView } from '@angular/material/datepicker';
import { Subject } from 'rxjs';
import * as moment from 'moment';
import { Moment } from 'moment';
@Component({
selector : 'treo-date-range',
templateUrl : './date-range.component.html',
styleUrls : ['./date-range.component.scss'],
encapsulation: ViewEncapsulation.None,
exportAs : 'treoDateRange',
providers : [
{
provide : NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TreoDateRangeComponent),
multi : true
}
]
})
export class TreoDateRangeComponent implements ControlValueAccessor, OnInit, OnDestroy
{
// Range changed
@Output()
readonly rangeChanged: EventEmitter<{ start: string, end: string }>;
activeDates: { month1: Moment, month2: Moment };
setWhichDate: 'start' | 'end';
startTimeFormControl: FormControl;
endTimeFormControl: FormControl;
// Private
@HostBinding('class.treo-date-range')
private _defaultClassNames;
@ViewChild('matMonthView1')
private _matMonthView1: MatMonthView<any>;
@ViewChild('matMonthView2')
private _matMonthView2: MatMonthView<any>;
@ViewChild('pickerPanelOrigin', {read: ElementRef})
private _pickerPanelOrigin: ElementRef;
@ViewChild('pickerPanel')
private _pickerPanel: TemplateRef<any>;
private _dateFormat: string;
private _onChange: (value: any) => void;
private _onTouched: (value: any) => void;
private _programmaticChange: boolean;
private _range: { start: Moment, end: Moment };
private _timeFormat: string;
private _timeRange: boolean;
private readonly _timeRegExp: RegExp;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {ChangeDetectorRef} _changeDetectorRef
* @param {ElementRef} _elementRef
* @param {Overlay} _overlay
* @param {Renderer2} _renderer2
* @param {ViewContainerRef} _viewContainerRef
*/
constructor(
private _changeDetectorRef: ChangeDetectorRef,
private _elementRef: ElementRef,
private _overlay: Overlay,
private _renderer2: Renderer2,
private _viewContainerRef: ViewContainerRef
)
{
// Set the private defaults
this._defaultClassNames = true;
this._onChange = () => {
};
this._onTouched = () => {
};
this._range = {
start: null,
end : null
};
this._timeRegExp = new RegExp('^(0[0-9]|1[0-9]|2[0-4]|[0-9]):([0-5][0-9])(A|(?:AM)|P|(?:PM))?$', 'i');
this._unsubscribeAll = new Subject();
// Set the defaults
this.activeDates = {
month1: null,
month2: null
};
this.dateFormat = 'DD/MM/YYYY';
this.rangeChanged = new EventEmitter();
this.setWhichDate = 'start';
this.timeFormat = '12';
// Initialize the component
this._init();
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for dateFormat input
*
* @param value
*/
@Input()
set dateFormat(value: string)
{
// Return, if the values are the same
if ( this._dateFormat === value )
{
return;
}
// Store the value
this._dateFormat = value;
}
get dateFormat(): string
{
return this._dateFormat;
}
/**
* Setter and getter for timeFormat input
*
* @param value
*/
@Input()
set timeFormat(value: string)
{
// Return, if the values are the same
if ( this._timeFormat === value )
{
return;
}
// Set format based on the time format input
this._timeFormat = value === '12' ? 'hh:mmA' : 'HH:mm';
}
get timeFormat(): string
{
return this._timeFormat;
}
/**
* Setter and getter for timeRange input
*
* @param value
*/
@Input()
set timeRange(value: boolean)
{
// Return, if the values are the same
if ( this._timeRange === value )
{
return;
}
// Store the value
this._timeRange = value;
// If the time range turned off...
if ( !value )
{
this.range = {
start: this._range.start.clone().startOf('day'),
end : this._range.end.clone().endOf('day')
};
}
}
get timeRange(): boolean
{
return this._timeRange;
}
/**
* Setter and getter for range input
*
* @param value
*/
@Input()
set range(value)
{
if ( !value )
{
return;
}
// Check if the value is an object and has 'start' and 'end' values
if ( !value.start || !value.end )
{
console.error('Range input must have "start" and "end" properties!');
return;
}
// Check if we are setting an individual date or both of them
const whichDate = value.whichDate || null;
// Get the start and end dates as moment
const start = moment(value.start);
const end = moment(value.end);
// If we are only setting the start date...
if ( whichDate === 'start' )
{
// Set the start date
this._range.start = start.clone();
// If the selected start date is after the end date...
if ( this._range.start.isAfter(this._range.end) )
{
// Set the end date to the start date but keep the end date's time
const endDate = start.clone().hours(this._range.end.hours()).minutes(this._range.end.minutes()).seconds(this._range.end.seconds());
// Test this new end date to see if it's ahead of the start date
if ( this._range.start.isBefore(endDate) )
{
// If it's, set the new end date
this._range.end = endDate;
}
else
{
// Otherwise, set the end date same as the start date
this._range.end = start.clone();
}
}
}
// If we are only setting the end date...
if ( whichDate === 'end' )
{
// Set the end date
this._range.end = end.clone();
// If the selected end date is before the start date...
if ( this._range.start.isAfter(this._range.end) )
{
// Set the start date to the end date but keep the start date's time
const startDate = end.clone().hours(this._range.start.hours()).minutes(this._range.start.minutes()).seconds(this._range.start.seconds());
// Test this new end date to see if it's ahead of the start date
if ( this._range.end.isAfter(startDate) )
{
// If it's, set the new start date
this._range.start = startDate;
}
else
{
// Otherwise, set the start date same as the end date
this._range.start = end.clone();
}
}
}
// If we are setting both dates...
if ( !whichDate )
{
// Set the start date
this._range.start = start.clone();
// If the start date is before the end date, set the end date as normal.
// If the start date is after the end date, set the end date same as the start date.
this._range.end = start.isBefore(end) ? end.clone() : start.clone();
}
// Prepare another range object that holds the ISO formatted range dates
const range = {
start: this._range.start.clone().toISOString(),
end : this._range.end.clone().toISOString()
};
// Emit the range changed event with the range
this.rangeChanged.emit(range);
// Update the model with the range if the change was not a programmatic change
// Because programmatic changes trigger writeValue which triggers onChange and onTouched
// internally causing them to trigger twice which breaks the form's pristine and touched
// statuses.
if ( !this._programmaticChange )
{
this._onTouched(range);
this._onChange(range);
}
// Set the active dates
this.activeDates = {
month1: this._range.start.clone(),
month2: this._range.start.clone().add(1, 'month')
};
// Set the time form controls
this.startTimeFormControl.setValue(this._range.start.clone().format(this._timeFormat).toString());
this.endTimeFormControl.setValue(this._range.end.clone().format(this._timeFormat).toString());
// Run ngAfterContentInit on month views to trigger
// re-render on month views if they are available
if ( this._matMonthView1 && this._matMonthView2 )
{
this._matMonthView1.ngAfterContentInit();
this._matMonthView2.ngAfterContentInit();
}
// Reset the programmatic change status
this._programmaticChange = false;
}
get range(): any
{
// Clone the range start and end
const start = this._range.start.clone();
const end = this._range.end.clone();
// Build and return the range object
return {
startDate: start.clone().format(this.dateFormat),
startTime: this.timeRange ? start.clone().format(this.timeFormat) : null,
endDate : end.clone().format(this.dateFormat),
endTime : this.timeRange ? end.clone().format(this.timeFormat) : null
};
}
// -----------------------------------------------------------------------------------------------------
// @ Control Value Accessor
// -----------------------------------------------------------------------------------------------------
/**
* Update the form model on change
*
* @param fn
*/
registerOnChange(fn: any): void
{
this._onChange = fn;
}
/**
* Update the form model on blur
*
* @param fn
*/
registerOnTouched(fn: any): void
{
this._onTouched = fn;
}
/**
* Write to view from model when the form model changes programmatically
*
* @param range
*/
writeValue(range: { start: string, end: string }): void
{
// Set this change as a programmatic one
this._programmaticChange = true;
// Set the range
this.range = range;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
// @ TODO: Workaround until "angular/issues/20007" resolved
this.writeValue = () => {
};
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Initialize
*
* @private
*/
private _init(): void
{
// Start and end time form controls
this.startTimeFormControl = new FormControl('', [Validators.pattern(this._timeRegExp)]);
this.endTimeFormControl = new FormControl('', [Validators.pattern(this._timeRegExp)]);
// Set the default range
this._programmaticChange = true;
this.range = {
start: moment().startOf('day').toISOString(),
end : moment().add(1, 'day').endOf('day').toISOString()
};
// Set the default time range
this._programmaticChange = true;
this.timeRange = true;
}
/**
* Parse the time from the inputs
*
* @param value
* @private
*/
private _parseTime(value: string): Moment
{
// Parse the time using the time regexp
const timeArr = value.split(this._timeRegExp).filter((part) => part !== '');
// Get the meridiem
const meridiem = timeArr[2] || null;
// If meridiem exists...
if ( meridiem )
{
// Create a moment using 12-hours format and return it
return moment(value, 'hh:mmA').seconds(0);
}
// If meridiem doesn't exist, create a moment using 24-hours format and return in
return moment(value, 'HH:mm').seconds(0);
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Open the picker panel
*/
openPickerPanel(): void
{
// Create the overlay
const overlayRef = this._overlay.create({
panelClass : 'treo-date-range-panel',
backdropClass : '',
hasBackdrop : true,
scrollStrategy : this._overlay.scrollStrategies.reposition(),
positionStrategy: this._overlay.position()
.flexibleConnectedTo(this._pickerPanelOrigin)
.withPositions([
{
originX : 'start',
originY : 'bottom',
overlayX: 'start',
overlayY: 'top',
offsetY : 8
},
{
originX : 'start',
originY : 'top',
overlayX: 'start',
overlayY: 'bottom',
offsetY : -8
}
])
});
// Create a portal from the template
const templatePortal = new TemplatePortal(this._pickerPanel, this._viewContainerRef);
// On backdrop click
overlayRef.backdropClick().subscribe(() => {
// If template portal exists and attached...
if ( templatePortal && templatePortal.isAttached )
{
// Detach it
templatePortal.detach();
}
// If overlay exists and attached...
if ( overlayRef && overlayRef.hasAttached() )
{
// Detach it
overlayRef.detach();
overlayRef.dispose();
}
});
// Attach the portal to the overlay
overlayRef.attach(templatePortal);
}
/**
* Get month label
*
* @param month
*/
getMonthLabel(month: number): string
{
if ( month === 1 )
{
return this.activeDates.month1.clone().format('MMMM Y');
}
return this.activeDates.month2.clone().format('MMMM Y');
}
/**
* Date class function to add/remove class names to calendar days
*/
dateClass(): any
{
return (date: Moment): MatCalendarCellCssClasses => {
// If the date is both start and end date...
if ( date.isSame(this._range.start, 'day') && date.isSame(this._range.end, 'day') )
{
return ['treo-date-range', 'treo-date-range-start', 'treo-date-range-end'];
}
// If the date is the start date...
if ( date.isSame(this._range.start, 'day') )
{
return ['treo-date-range', 'treo-date-range-start'];
}
// If the date is the end date...
if ( date.isSame(this._range.end, 'day') )
{
return ['treo-date-range', 'treo-date-range-end'];
}
// If the date is in between start and end dates...
if ( date.isBetween(this._range.start, this._range.end, 'day') )
{
return ['treo-date-range', 'treo-date-range-mid'];
}
return undefined;
};
}
/**
* Date filter to enable/disable calendar days
*/
dateFilter(): any
{
return (date: Moment): boolean => {
// If we are selecting the end date, disable all the dates that comes before the start date
return !(this.setWhichDate === 'end' && date.isBefore(this._range.start, 'day'));
};
}
/**
* On selected date change
*
* @param date
*/
onSelectedDateChange(date: Moment): void
{
// Create a new range object
const newRange = {
start : this._range.start.clone().toISOString(),
end : this._range.end.clone().toISOString(),
whichDate: null
};
// Replace either the start or the end date with the new one
// depending on which date we are setting
if ( this.setWhichDate === 'start' )
{
newRange.start = moment(newRange.start).year(date.year()).month(date.month()).date(date.date()).toISOString();
}
else
{
newRange.end = moment(newRange.end).year(date.year()).month(date.month()).date(date.date()).toISOString();
}
// Append the which date to the new range object
newRange.whichDate = this.setWhichDate;
// Switch which date to set on the next run
this.setWhichDate = this.setWhichDate === 'start' ? 'end' : 'start';
// Set the range
this.range = newRange;
}
/**
* Go to previous month on both views
*/
prev(): void
{
this.activeDates.month1 = moment(this.activeDates.month1).subtract(1, 'month');
this.activeDates.month2 = moment(this.activeDates.month2).subtract(1, 'month');
}
/**
* Go to next month on both views
*/
next(): void
{
this.activeDates.month1 = moment(this.activeDates.month1).add(1, 'month');
this.activeDates.month2 = moment(this.activeDates.month2).add(1, 'month');
}
/**
* Update the start time
*
* @param event
*/
updateStartTime(event): void
{
// Parse the time
const parsedTime = this._parseTime(event.target.value);
// Go back to the previous value if the form control is not valid
if ( this.startTimeFormControl.invalid )
{
// Override the time
const time = this._range.start.clone().format(this._timeFormat);
// Set the time
this.startTimeFormControl.setValue(time);
// Do not update the range
return;
}
// Append the new time to the start date
const startDate = this._range.start.clone().hours(parsedTime.hours()).minutes(parsedTime.minutes());
// If the new start date is after the current end date,
// use the end date's time and set the start date again
if ( startDate.isAfter(this._range.end) )
{
const endDateHours = this._range.end.hours();
const endDateMinutes = this._range.end.minutes();
// Set the start date
startDate.hours(endDateHours).minutes(endDateMinutes);
}
// If everything is okay, set the new date
this.range = {
start : startDate.toISOString(),
end : this._range.end.clone().toISOString(),
whichDate: 'start'
};
}
/**
* Update the end time
*
* @param event
*/
updateEndTime(event): void
{
// Parse the time
const parsedTime = this._parseTime(event.target.value);
// Go back to the previous value if the form control is not valid
if ( this.endTimeFormControl.invalid )
{
// Override the time
const time = this._range.end.clone().format(this._timeFormat);
// Set the time
this.endTimeFormControl.setValue(time);
// Do not update the range
return;
}
// Append the new time to the end date
const endDate = this._range.end.clone().hours(parsedTime.hours()).minutes(parsedTime.minutes());
// If the new end date is before the current start date,
// use the start date's time and set the end date again
if ( endDate.isBefore(this._range.start) )
{
const startDateHours = this._range.start.hours();
const startDateMinutes = this._range.start.minutes();
// Set the end date
endDate.hours(startDateHours).minutes(startDateMinutes);
}
// If everything is okay, set the new date
this.range = {
start : this._range.start.clone().toISOString(),
end : endDate.toISOString(),
whichDate: 'end'
};
}
}
@@ -0,0 +1,32 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMomentDateModule } from '@angular/material-moment-adapter';
import { TreoDateRangeComponent } from '@treo/components/date-range/date-range.component';
@NgModule({
declarations: [
TreoDateRangeComponent
],
imports : [
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatDatepickerModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatMomentDateModule
],
exports : [
TreoDateRangeComponent
]
})
export class TreoDateRangeModule
{
}
@@ -0,0 +1 @@
export * from '@treo/components/date-range/public-api';
@@ -0,0 +1,2 @@
export * from '@treo/components/date-range/date-range.component';
export * from '@treo/components/date-range/date-range.module';
@@ -0,0 +1,3 @@
<div class="treo-drawer-content">
<ng-content></ng-content>
</div>
@@ -0,0 +1,146 @@
@import 'treo';
$treo-drawer-width: 320;
treo-drawer {
position: relative;
display: flex;
flex-direction: column;
flex: 1 1 auto;
width: #{$treo-drawer-width}px;
min-width: #{$treo-drawer-width}px;
max-width: #{$treo-drawer-width}px;
z-index: 300;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, .35);
// Animations
&.treo-drawer-animations-enabled {
transition-duration: 400ms;
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1);
transition-property: visibility, margin-left, margin-right, transform, width, max-width, min-width;
.treo-drawer-content {
transition-duration: 400ms;
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1);
transition-property: width, max-width, min-width;
}
}
// Over mode
&.treo-drawer-mode-over {
position: absolute;
top: 0;
bottom: 0;
// Fixed mode
&.treo-drawer-fixed {
position: fixed;
}
}
// Left position
&.treo-drawer-position-left {
// Side mode
&.treo-drawer-mode-side {
margin-left: #{$treo-drawer-width}px;
&.treo-drawer-opened {
margin-left: 0;
}
}
// Over mode
&.treo-drawer-mode-over {
left: 0;
transform: translate3d(-100%, 0, 0);
&.treo-drawer-opened {
transform: translate3d(0, 0, 0);
}
}
// Content
.treo-drawer-content {
left: 0;
}
}
// Right position
&.treo-drawer-position-right {
// Side mode
&.treo-drawer-mode-side {
margin-right: -#{$treo-drawer-width}px;
&.treo-drawer-opened {
margin-right: 0;
}
}
// Over mode
&.treo-drawer-mode-over {
right: 0;
transform: translate3d(100%, 0, 0);
&.treo-drawer-opened {
transform: translate3d(0, 0, 0);
}
}
// Content
.treo-drawer-content {
right: 0;
}
}
// Content
.treo-drawer-content {
position: absolute;
display: flex;
flex: 1 1 auto;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
}
// Overlay
.treo-drawer-overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 299;
opacity: 0;
background-color: rgba(0, 0, 0, 0.6);
// Fixed mode
&.treo-drawer-overlay-fixed {
position: fixed;
}
// Transparent overlay
&.treo-drawer-overlay-transparent {
background-color: transparent;
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
treo-drawer {
background: map-get($background, card);
.treo-drawer-content {
background: map-get($background, card);
}
}
}
@@ -0,0 +1,523 @@
import { Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnDestroy, OnInit, Output, Renderer2, ViewEncapsulation } from '@angular/core';
import { animate, AnimationBuilder, AnimationPlayer, style } from '@angular/animations';
import { TreoDrawerMode, TreoDrawerPosition } from '@treo/components/drawer/drawer.types';
import { TreoDrawerService } from '@treo/components/drawer/drawer.service';
@Component({
selector : 'treo-drawer',
templateUrl : './drawer.component.html',
styleUrls : ['./drawer.component.scss'],
encapsulation: ViewEncapsulation.None,
exportAs : 'treoDrawer'
})
export class TreoDrawerComponent implements OnInit, OnDestroy
{
// Name
@Input()
name: string;
// Private
private _fixed: boolean;
private _mode: TreoDrawerMode;
private _opened: boolean | '';
private _overlay: HTMLElement | null;
private _player: AnimationPlayer;
private _position: TreoDrawerPosition;
private _transparentOverlay: boolean | '';
// On fixed changed
@Output()
readonly fixedChanged: EventEmitter<boolean>;
// On mode changed
@Output()
readonly modeChanged: EventEmitter<TreoDrawerMode>;
// On opened changed
@Output()
readonly openedChanged: EventEmitter<boolean | ''>;
// On position changed
@Output()
readonly positionChanged: EventEmitter<TreoDrawerPosition>;
@HostBinding('class.treo-drawer-animations-enabled')
private _animationsEnabled: boolean;
/**
* Constructor
*
* @param {AnimationBuilder} _animationBuilder
* @param {TreoDrawerService} _treoDrawerService
* @param {ElementRef} _elementRef
* @param {Renderer2} _renderer2
*/
constructor(
private _animationBuilder: AnimationBuilder,
private _treoDrawerService: TreoDrawerService,
private _elementRef: ElementRef,
private _renderer2: Renderer2
)
{
// Set the private defaults
this._animationsEnabled = false;
this._overlay = null;
// Set the defaults
this.fixedChanged = new EventEmitter<boolean>();
this.modeChanged = new EventEmitter<TreoDrawerMode>();
this.openedChanged = new EventEmitter<boolean | ''>();
this.positionChanged = new EventEmitter<TreoDrawerPosition>();
this.fixed = false;
this.mode = 'side';
this.opened = false;
this.position = 'left';
this.transparentOverlay = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter & getter for fixed
*
* @param value
*/
@Input()
set fixed(value: boolean)
{
// If the value is the same, return...
if ( this._fixed === value )
{
return;
}
// Store the fixed value
this._fixed = value;
// Update the class
if ( this.fixed )
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-drawer-fixed');
}
else
{
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-drawer-fixed');
}
// Execute the observable
this.fixedChanged.next(this.fixed);
}
get fixed(): boolean
{
return this._fixed;
}
/**
* Setter & getter for mode
*
* @param value
*/
@Input()
set mode(value: TreoDrawerMode)
{
// If the value is the same, return...
if ( this._mode === value )
{
return;
}
// Disable the animations
this._disableAnimations();
// If the mode changes: 'over -> side'
if ( this.mode === 'over' && value === 'side' )
{
// Hide the overlay
this._hideOverlay();
}
// If the mode changes: 'side -> over'
if ( this.mode === 'side' && value === 'over' )
{
// If the drawer is opened
if ( this.opened )
{
// Show the overlay
this._showOverlay();
}
}
let modeClassName;
// Remove the previous mode class
modeClassName = 'treo-drawer-mode-' + this.mode;
this._renderer2.removeClass(this._elementRef.nativeElement, modeClassName);
// Store the mode
this._mode = value;
// Add the new mode class
modeClassName = 'treo-drawer-mode-' + this.mode;
this._renderer2.addClass(this._elementRef.nativeElement, modeClassName);
// Execute the observable
this.modeChanged.next(this.mode);
// Enable the animations after a delay
// The delay must be bigger than the current transition-duration
// to make sure nothing will be animated while the mode changing
setTimeout(() => {
this._enableAnimations();
}, 500);
}
get mode(): TreoDrawerMode
{
return this._mode;
}
/**
* Setter & getter for opened
*
* @param value
*/
@Input()
set opened(value: boolean | '')
{
// If the value is the same, return...
if ( this._opened === value )
{
return;
}
// If the provided value is an empty string,
// take that as a 'true'
if ( value === '' )
{
value = true;
}
// Set the opened value
this._opened = value;
// If the drawer opened, and the mode
// is 'over', show the overlay
if ( this.mode === 'over' )
{
if ( this._opened )
{
this._showOverlay();
}
else
{
this._hideOverlay();
}
}
// Update opened classes
if ( this.opened )
{
this._renderer2.setStyle(this._elementRef.nativeElement, 'visibility', 'visible');
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-drawer-opened');
}
else
{
this._renderer2.setStyle(this._elementRef.nativeElement, 'visibility', 'hidden');
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-drawer-opened');
}
// Execute the observable
this.openedChanged.next(this.opened);
}
get opened(): boolean | ''
{
return this._opened;
}
/**
* Setter & getter for position
*
* @param value
*/
@Input()
set position(value: TreoDrawerPosition)
{
// If the value is the same, return...
if ( this._position === value )
{
return;
}
let positionClassName;
// Remove the previous position class
positionClassName = 'treo-drawer-position-' + this.position;
this._renderer2.removeClass(this._elementRef.nativeElement, positionClassName);
// Store the position
this._position = value;
// Add the new position class
positionClassName = 'treo-drawer-position-' + this.position;
this._renderer2.addClass(this._elementRef.nativeElement, positionClassName);
// Execute the observable
this.positionChanged.next(this.position);
}
get position(): TreoDrawerPosition
{
return this._position;
}
/**
* Setter & getter for transparent overlay
*
* @param value
*/
@Input()
set transparentOverlay(value: boolean | '')
{
// If the value is the same, return...
if ( this._opened === value )
{
return;
}
// If the provided value is an empty string,
// take that as a 'true' and set the opened value
if ( value === '' )
{
// Set the opened value
this._transparentOverlay = true;
}
else
{
// Set the transparent overlay value
this._transparentOverlay = value;
}
}
get transparentOverlay(): boolean | ''
{
return this._transparentOverlay;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Register the drawer
this._treoDrawerService.registerComponent(this.name, this);
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Deregister the drawer from the registry
this._treoDrawerService.deregisterComponent(this.name);
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Enable the animations
*
* @private
*/
private _enableAnimations(): void
{
// If the animations are already enabled, return...
if ( this._animationsEnabled )
{
return;
}
// Enable the animations
this._animationsEnabled = true;
}
/**
* Disable the animations
*
* @private
*/
private _disableAnimations(): void
{
// If the animations are already disabled, return...
if ( !this._animationsEnabled )
{
return;
}
// Disable the animations
this._animationsEnabled = false;
}
/**
* Show the backdrop
*
* @private
*/
private _showOverlay(): void
{
// Create the backdrop element
this._overlay = this._renderer2.createElement('div');
// Add a class to the backdrop element
this._overlay.classList.add('treo-drawer-overlay');
// Add a class depending on the fixed option
if ( this.fixed )
{
this._overlay.classList.add('treo-drawer-overlay-fixed');
}
// Add a class depending on the transparentOverlay option
if ( this.transparentOverlay )
{
this._overlay.classList.add('treo-drawer-overlay-transparent');
}
// Append the backdrop to the parent of the drawer
this._renderer2.appendChild(this._elementRef.nativeElement.parentElement, this._overlay);
// Create the enter animation and attach it to the player
this._player =
this._animationBuilder
.build([
animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 1}))
]).create(this._overlay);
// Play the animation
this._player.play();
// Add an event listener to the overlay
this._overlay.addEventListener('click', () => {
this.close();
});
}
/**
* Hide the backdrop
*
* @private
*/
private _hideOverlay(): void
{
if ( !this._overlay )
{
return;
}
// Create the leave animation and attach it to the player
this._player =
this._animationBuilder
.build([
animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 0}))
]).create(this._overlay);
// Play the animation
this._player.play();
// Once the animation is done...
this._player.onDone(() => {
// If the backdrop still exists...
if ( this._overlay )
{
// Remove the backdrop
this._overlay.parentNode.removeChild(this._overlay);
this._overlay = null;
}
});
}
/**
* On mouseenter
*
* @private
*/
@HostListener('mouseenter')
private _onMouseenter(): void
{
// Enable the animations
this._enableAnimations();
// Add a class
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-drawer-hover');
}
/**
* On mouseleave
*
* @private
*/
@HostListener('mouseleave')
private _onMouseleave(): void
{
// Enable the animations
this._enableAnimations();
// Remove the class
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-drawer-hover');
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Open the drawer
*/
open(): void
{
// Enable the animations
this._enableAnimations();
// Open
this.opened = true;
}
/**
* Close the drawer
*/
close(): void
{
// Enable the animations
this._enableAnimations();
// Close
this.opened = false;
}
/**
* Toggle the opened status
*/
toggle(): void
{
// Toggle
if ( this.opened )
{
this.close();
}
else
{
this.open();
}
}
}
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TreoDrawerComponent } from '@treo/components/drawer/drawer.component';
@NgModule({
declarations: [
TreoDrawerComponent
],
imports : [
CommonModule
],
exports : [
TreoDrawerComponent
]
})
export class TreoDrawerModule
{
}
@@ -0,0 +1,55 @@
import { Injectable } from '@angular/core';
import { TreoDrawerComponent } from '@treo/components/drawer/drawer.component';
@Injectable({
providedIn: 'root'
})
export class TreoDrawerService
{
// Private
private _componentRegistry: Map<string, TreoDrawerComponent>;
/**
* Constructor
*/
constructor()
{
// Set the defaults
this._componentRegistry = new Map<string, TreoDrawerComponent>();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Register drawer component
*
* @param name
* @param component
*/
registerComponent(name: string, component: TreoDrawerComponent): void
{
this._componentRegistry.set(name, component);
}
/**
* Deregister drawer component
*
* @param name
*/
deregisterComponent(name: string): void
{
this._componentRegistry.delete(name);
}
/**
* Get drawer component from the registry
*
* @param name
*/
getComponent(name: string): TreoDrawerComponent
{
return this._componentRegistry.get(name);
}
}
@@ -0,0 +1,2 @@
export type TreoDrawerMode = 'over' | 'side';
export type TreoDrawerPosition = 'left' | 'right';
@@ -0,0 +1 @@
export * from '@treo/components/drawer/public-api';
@@ -0,0 +1,4 @@
export * from '@treo/components/drawer/drawer.component';
export * from '@treo/components/drawer/drawer.module';
export * from '@treo/components/drawer/drawer.service';
export * from '@treo/components/drawer/drawer.types';
@@ -0,0 +1,9 @@
<ng-content></ng-content>
<!-- @formatter:off -->
<ng-template let-highlightedCode="highlightedCode" let-lang="lang">
<div class="treo-highlight treo-highlight-code-container">
<pre [ngClass]="'language-' + lang"><code [ngClass]="'language-' + lang" [innerHTML]="highlightedCode"></code></pre>
</div>
</ng-template>
<!-- @formatter:on -->
@@ -0,0 +1,3 @@
textarea[treo-highlight] {
display: none;
}
@@ -0,0 +1,175 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EmbeddedViewRef, Input, Renderer2, SecurityContext, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { TreoHighlightService } from '@treo/components/highlight/highlight.service';
@Component({
selector : 'textarea[treo-highlight]',
templateUrl : './highlight.component.html',
styleUrls : ['./highlight.component.scss'],
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
exportAs : 'treoHighlight'
})
export class TreoHighlightComponent implements AfterViewInit
{
highlightedCode: string;
viewRef: EmbeddedViewRef<any>;
@ViewChild(TemplateRef)
templateRef: TemplateRef<any>;
// Private
private _code: string;
private _lang: string;
/**
* Constructor
*
* @param {TreoHighlightService} _treoHighlightService
* @param {DomSanitizer} _domSanitizer
* @param {ChangeDetectorRef} _changeDetectorRef
* @param {ElementRef} _elementRef
* @param {Renderer2} _renderer2
* @param {ViewContainerRef} _viewContainerRef
*/
constructor(
private _treoHighlightService: TreoHighlightService,
private _domSanitizer: DomSanitizer,
private _changeDetectorRef: ChangeDetectorRef,
private _elementRef: ElementRef,
private _renderer2: Renderer2,
private _viewContainerRef: ViewContainerRef
)
{
// Set the private defaults
this._code = '';
this._lang = '';
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for the code
*/
@Input()
set code(value: string)
{
// If the value is the same, return...
if ( this._code === value )
{
return;
}
// Set the code
this._code = value;
// Highlight and insert the code if the
// viewContainerRef is available. This will
// ensure the highlightAndInsert method
// won't run before the AfterContentInit hook.
if ( this._viewContainerRef.length )
{
this._highlightAndInsert();
}
}
get code(): string
{
return this._code;
}
/**
* Setter and getter for the language
*/
@Input()
set lang(value: string)
{
// If the value is the same, return...
if ( this._lang === value )
{
return;
}
// Set the language
this._lang = value;
// Highlight and insert the code if the
// viewContainerRef is available. This will
// ensure the highlightAndInsert method
// won't run before the AfterContentInit hook.
if ( this._viewContainerRef.length )
{
this._highlightAndInsert();
}
}
get lang(): string
{
return this._lang;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* After view init
*/
ngAfterViewInit(): void
{
// Return, if there is no language set
if ( !this.lang )
{
return;
}
// If there is no code input, get the code from
// the textarea
if ( !this.code )
{
// Get the code
this.code = this._elementRef.nativeElement.value;
}
// Highlight and insert
this._highlightAndInsert();
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Highlight and insert the highlighted code
*
* @private
*/
private _highlightAndInsert(): void
{
// Return, if the code or language is not defined
if ( !this.code || !this.lang )
{
return;
}
// Destroy the component if there is already one
if ( this.viewRef )
{
this.viewRef.destroy();
}
// Highlight and sanitize the code just in case
this.highlightedCode = this._domSanitizer.sanitize(SecurityContext.HTML, this._treoHighlightService.highlight(this.code, this.lang));
// Render and insert the template
this.viewRef = this._viewContainerRef.createEmbeddedView(this.templateRef, {
highlightedCode: this.highlightedCode,
lang : this.lang
});
// Detect the changes
this.viewRef.detectChanges();
}
}
@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TreoHighlightComponent } from '@treo/components/highlight/highlight.component';
@NgModule({
declarations : [
TreoHighlightComponent
],
imports : [
CommonModule
],
exports : [
TreoHighlightComponent
],
entryComponents: [
TreoHighlightComponent
]
})
export class TreoHighlightModule
{
}
@@ -0,0 +1,87 @@
import { Injectable } from '@angular/core';
import * as hljs from 'highlight.js';
@Injectable({
providedIn: 'root'
})
export class TreoHighlightService
{
/**
* Constructor
*/
constructor()
{
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Remove the empty lines around the code block
* and re-align the indentation based on the first
* non-whitespace indented character
*
* @param code
* @private
*/
private _format(code: string): string
{
let firstCharIndentation: number | null = null;
// Split the code into lines and store the lines
const lines = code.split('\n');
// Trim the empty lines around the code block
while ( lines.length && lines[0].trim() === '' )
{
lines.shift();
}
while ( lines.length && lines[lines.length - 1].trim() === '' )
{
lines.pop();
}
// Iterate through the lines to figure out the first
// non-whitespace character indentation
lines.forEach((line) => {
// Skip the line if its length is zero
if ( line.length === 0 )
{
return;
}
// We look at all the lines to find the smallest indentation
// of the first non-whitespace char since the first ever line
// is not necessarily has to be the line with the smallest
// non-whitespace char indentation
firstCharIndentation = firstCharIndentation === null ?
line.search(/\S|$/) :
Math.min(line.search(/\S|$/), firstCharIndentation);
});
// Iterate through the lines one more time, remove the extra
// indentation, join them together and return it
return lines.map((line) => {
return line.substring(firstCharIndentation);
}).join('\n');
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Highlight
*/
highlight(code: string, language: string): string
{
// Format the code
code = this._format(code);
// Highlight and return the code
return hljs.highlight(language, code).value;
}
}
@@ -0,0 +1 @@
export * from '@treo/components/highlight/public-api';
@@ -0,0 +1,3 @@
export * from '@treo/components/highlight/highlight.component';
export * from '@treo/components/highlight/highlight.module';
export * from '@treo/components/highlight/highlight.service';
@@ -0,0 +1 @@
export * from '@treo/components/message/public-api';
@@ -0,0 +1,70 @@
<div class="treo-message-container"
*ngIf="!dismissed"
[@fadeIn]="dismissed === null ? false : !dismissed"
[@fadeOut]="dismissed === null ? false : !dismissed">
<!-- Icon -->
<div class="treo-message-icon"
*ngIf="showIcon">
<!-- Custom icon -->
<div class="treo-message-custom-icon">
<ng-content select="[treoMessageIcon]"></ng-content>
</div>
<!-- Default icons -->
<div class="treo-message-default-icon">
<mat-icon *ngIf="type === 'primary'"
[svgIcon]="'check_circle'"></mat-icon>
<mat-icon *ngIf="type === 'accent'"
[svgIcon]="'check_circle'"></mat-icon>
<mat-icon *ngIf="type === 'warn'"
[svgIcon]="'warning'"></mat-icon>
<mat-icon *ngIf="type === 'basic'"
[svgIcon]="'check_circle'"></mat-icon>
<mat-icon *ngIf="type === 'info'"
[svgIcon]="'info'"></mat-icon>
<mat-icon *ngIf="type === 'success'"
[svgIcon]="'check_circle'"></mat-icon>
<mat-icon *ngIf="type === 'warning'"
[svgIcon]="'warning'"></mat-icon>
<mat-icon *ngIf="type === 'error'"
[svgIcon]="'error'"></mat-icon>
</div>
</div>
<!-- Content -->
<div class="treo-message-content">
<div class="treo-message-title">
<p>
<ng-content select="[treoMessageTitle]"></ng-content>
</p>
</div>
<div class="treo-message-message">
<p>
<ng-content></ng-content>
</p>
</div>
</div>
<!-- Dismiss button -->
<button class="treo-message-dismiss-button"
mat-icon-button
(click)="dismiss()">
<mat-icon [svgIcon]="'close'"></mat-icon>
</button>
</div>
@@ -0,0 +1,592 @@
@import 'treo';
treo-message {
display: block;
// Show icon
&.treo-message-show-icon {
.treo-message-container {
padding-left: 56px;
}
}
// Dismissible
&.treo-message-dismissible {
.treo-message-container {
padding-right: 56px;
}
}
// Common
.treo-message-container {
position: relative;
display: flex;
min-height: 64px;
padding: 16px 24px;
font-size: 14px;
line-height: 1;
// Icon
.treo-message-icon {
position: absolute;
top: 20px;
left: 17px;
.treo-message-custom-icon,
.treo-message-default-icon {
display: none;
align-items: center;
justify-content: center;
border-radius: 50%;
&:not(:empty) {
display: flex;
}
}
.treo-message-custom-icon {
display: none;
&:not(:empty) {
display: flex;
+ .treo-message-default-icon {
display: none;
}
}
}
}
// Content
.treo-message-content {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1;
// Title
.treo-message-title {
display: none;
font-size: 15px;
font-weight: 600;
line-height: 1.2;
&:not(:empty) {
display: block;
}
p {
line-height: 1.625;
}
}
// Message
.treo-message-message {
display: none;
&:not(:empty) {
display: block;
}
p {
line-height: 1.625;
}
}
}
// Dismiss button
.treo-message-dismiss-button {
position: absolute;
top: 12px;
right: 12px;
width: 32px !important;
min-width: 32px !important;
height: 32px !important;
min-height: 32px !important;
line-height: 32px !important;
margin-left: auto;
.mat-icon {
@include treo-icon-size(20);
}
}
}
// Dismissible
&:not(.treo-message-dismissible) {
.treo-message-container {
.treo-message-dismiss-button {
display: none !important;
}
}
}
// Border
&.treo-message-appearance-border {
.treo-message-container {
overflow: hidden;
border-left-width: 4px;
border-radius: 4px;
@include treo-elevation('xl');
}
}
// Fill
&.treo-message-appearance-fill {
.treo-message-container {
border-radius: 4px;
}
}
// Outline
&.treo-message-appearance-outline {
.treo-message-container {
overflow: hidden;
border-radius: 4px;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 6px;
}
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$is-dark: map-get($theme, is-dark);
treo-message {
.treo-message-container {
// Icon
.mat-icon {
color: currentColor;
}
}
// Border
&.treo-message-appearance-border {
.treo-message-container {
background: map-get($background, card);
.treo-message-message {
color: map-get($foreground, secondary-text);
}
}
// Primary
&.treo-message-type-primary {
.treo-message-container {
border-left-color: map-get($primary, default);
.treo-message-title,
.treo-message-icon {
color: map-get($primary, default);
}
}
}
// Accent
&.treo-message-type-accent {
.treo-message-container {
border-left-color: map-get($accent, default);
.treo-message-title,
.treo-message-icon {
color: map-get($accent, default);
}
}
}
// Warn
&.treo-message-type-warn {
.treo-message-container {
border-left-color: map-get($warn, default);
.treo-message-title,
.treo-message-icon {
color: map-get($warn, default);
}
}
}
// Basic
&.treo-message-type-basic {
.treo-message-container {
border-left-color: treo-color('cool-gray', 600);
.treo-message-title,
.treo-message-icon {
color: treo-color('cool-gray', 600);
}
}
}
// Info
&.treo-message-type-info {
.treo-message-container {
border-left-color: treo-color('blue', 600);
.treo-message-title,
.treo-message-icon {
color: treo-color('blue', 700);
}
}
}
// Success
&.treo-message-type-success {
.treo-message-container {
border-left-color: treo-color('green', 500);
.treo-message-title,
.treo-message-icon {
color: treo-color('green', 500);
}
}
}
// Warning
&.treo-message-type-warning {
.treo-message-container {
border-left-color: treo-color('yellow', 400);
.treo-message-title,
.treo-message-icon {
color: treo-color('yellow', 400);
}
}
}
// Error
&.treo-message-type-error {
.treo-message-container {
border-left-color: treo-color('red', 600);
.treo-message-title,
.treo-message-icon {
color: treo-color('red', 700);
}
}
}
}
// Fill
&.treo-message-appearance-fill {
// Primary
&.treo-message-type-primary {
.treo-message-container {
background: map-get($primary, default);
color: map-get($primary, default-contrast);
code {
background: map-get($primary, 600);
color: map-get($primary, '600-contrast');
}
}
}
// Accent
&.treo-message-type-accent {
.treo-message-container {
background: map-get($accent, default);
color: map-get($accent, default-contrast);
code {
background: map-get($accent, 600);
color: map-get($accent, '600-contrast');
}
}
}
// Warn
&.treo-message-type-warn {
.treo-message-container {
background: map-get($warn, default);
color: map-get($warn, default-contrast);
code {
background: map-get($warn, 800);
color: map-get($warn, '800-contrast');
}
}
}
// Basic
&.treo-message-type-basic {
.treo-message-container {
background: treo-color('cool-gray', 500);
color: treo-color('cool-gray', 50);
code {
background: treo-color('cool-gray', 600);
color: treo-color('cool-gray', 50);
}
}
}
// Info
&.treo-message-type-info {
.treo-message-container {
background: treo-color('blue', 600);
color: treo-color('blue', 50);
code {
background: treo-color('blue', 800);
color: treo-color('blue', 50);
}
}
}
// Success
&.treo-message-type-success {
.treo-message-container {
background: treo-color('green', 500);
color: treo-color('green', 50);
code {
background: treo-color('green', 600);
color: treo-color('green', 50);
}
}
}
// Warning
&.treo-message-type-warning {
.treo-message-container {
background: treo-color('yellow', 400);
color: treo-color('yellow', 50);
code {
background: treo-color('yellow', 600);
color: treo-color('yellow', 50);
}
}
}
// Error
&.treo-message-type-error {
.treo-message-container {
background: treo-color('red', 600);
color: treo-color('red', 50);
code {
background: treo-color('red', 800);
color: treo-color('red', 50);
}
}
}
}
// Outline
&.treo-message-appearance-outline {
// Primary
&.treo-message-type-primary {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: map-get($primary, 300);
box-shadow: inset 0 0 0 1px map-get($primary, 300);
} @else {
background: map-get($primary, 50);
color: map-get($primary, 800);
box-shadow: inset 0 0 0 1px map-get($primary, 400);
}
code {
background: map-get($primary, 200);
color: map-get($primary, 800);
}
}
}
// Accent
&.treo-message-type-accent {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: map-get($accent, 300);
box-shadow: inset 0 0 0 1px map-get($accent, 300);
} @else {
background: map-get($accent, 50);
color: map-get($accent, 800);
box-shadow: inset 0 0 0 1px map-get($accent, 400);
}
code {
background: map-get($accent, 200);
color: map-get($accent, 800);
}
}
}
// Warn
&.treo-message-type-warn {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: map-get($warn, 300);
box-shadow: inset 0 0 0 1px map-get($warn, 300);
} @else {
background: map-get($warn, 50);
color: map-get($warn, 800);
box-shadow: inset 0 0 0 1px map-get($warn, 400);
}
code {
background: map-get($warn, 200);
color: map-get($warn, 800);
}
}
}
// Basic
&.treo-message-type-basic {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: treo-color('cool-gray', 300);
box-shadow: inset 0 0 0 1px treo-color('cool-gray', 300);
} @else {
background: treo-color('cool-gray', 50);
color: treo-color('cool-gray', 800);
box-shadow: inset 0 0 0 1px treo-color('cool-gray', 400);
}
code {
background: treo-color('cool-gray', 200);
color: treo-color('cool-gray', 800);
}
}
}
// Info
&.treo-message-type-info {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: treo-color('blue', 300);
box-shadow: inset 0 0 0 1px treo-color('blue', 300);
} @else {
background: treo-color('blue', 50);
color: treo-color('blue', 800);
box-shadow: inset 0 0 0 1px treo-color('blue', 400);
}
code {
background: treo-color('blue', 200);
color: treo-color('blue', 800);
}
}
}
// Success
&.treo-message-type-success {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: treo-color('green', 300);
box-shadow: inset 0 0 0 1px treo-color('green', 300);
} @else {
background: treo-color('green', 50);
color: treo-color('green', 800);
box-shadow: inset 0 0 0 1px treo-color('green', 400);
}
code {
background: treo-color('green', 200);
color: treo-color('green', 800);
}
}
}
// Warning
&.treo-message-type-warning {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: treo-color('yellow', 300);
box-shadow: inset 0 0 0 1px treo-color('yellow', 300);
} @else {
background: treo-color('yellow', 50);
color: treo-color('yellow', 800);
box-shadow: inset 0 0 0 1px treo-color('yellow', 400);
}
code {
background: treo-color('yellow', 200);
color: treo-color('yellow', 800);
}
}
}
// Error
&.treo-message-type-error {
.treo-message-container {
@if ($is-dark) {
background: transparent;
color: treo-color('red', 500);
box-shadow: inset 0 0 0 1px treo-color('red', 500);
} @else {
background: treo-color('red', 50);
color: treo-color('red', 800);
box-shadow: inset 0 0 0 1px treo-color('red', 400);
}
code {
background: treo-color('red', 200);
color: treo-color('red', 800);
}
}
}
}
}
}
@@ -0,0 +1,287 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, Renderer2, ViewEncapsulation } from '@angular/core';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { TreoAnimations } from '@treo/animations';
import { TreoMessageAppearance, TreoMessageType } from '@treo/components/message/message.types';
import { TreoMessageService } from '@treo/components/message/message.service';
@Component({
selector : 'treo-message',
templateUrl : './message.component.html',
styleUrls : ['./message.component.scss'],
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
animations : TreoAnimations,
exportAs : 'treoMessage'
})
export class TreoMessageComponent implements OnInit, OnDestroy
{
// Name
@Input()
name: string;
@Output()
readonly afterDismissed: EventEmitter<boolean>;
@Output()
readonly afterShown: EventEmitter<boolean>;
// Private
private _appearance: TreoMessageAppearance;
private _dismissed: null | boolean;
private _showIcon: boolean;
private _type: TreoMessageType;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoMessageService} _treoMessageService
* @param {ChangeDetectorRef} _changeDetectorRef
* @param {ElementRef} _elementRef
* @param {Renderer2} _renderer2
*/
constructor(
private _treoMessageService: TreoMessageService,
private _changeDetectorRef: ChangeDetectorRef,
private _elementRef: ElementRef,
private _renderer2: Renderer2
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.afterDismissed = new EventEmitter<boolean>();
this.afterShown = new EventEmitter<boolean>();
this.appearance = 'fill';
this.dismissed = null;
this.showIcon = true;
this.type = 'primary';
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter and getter for appearance
*
* @param value
*/
@Input()
set appearance(value: TreoMessageAppearance)
{
// If the value is the same, return...
if ( this._appearance === value )
{
return;
}
// Update the class name
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-appearance-' + this.appearance);
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-appearance-' + value);
// Store the value
this._appearance = value;
}
get appearance(): TreoMessageAppearance
{
return this._appearance;
}
/**
* Setter and getter for dismissed
*
* @param value
*/
@Input()
set dismissed(value: null | boolean)
{
// If the value is the same, return...
if ( this._dismissed === value )
{
return;
}
// Update the class name
if ( value === null )
{
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-dismissible');
}
else if ( value === false )
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-dismissible');
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-dismissed');
}
else
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-dismissible');
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-dismissed');
}
// Store the value
this._dismissed = value;
}
get dismissed(): null | boolean
{
return this._dismissed;
}
/**
* Setter and getter for show icon
*
* @param value
*/
@Input()
set showIcon(value: boolean)
{
// If the value is the same, return...
if ( this._showIcon === value )
{
return;
}
// Update the class name
if ( value )
{
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-show-icon');
}
else
{
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-show-icon');
}
// Store the value
this._showIcon = value;
}
get showIcon(): boolean
{
return this._showIcon;
}
/**
* Setter and getter for type
*
* @param value
*/
@Input()
set type(value: TreoMessageType)
{
// If the value is the same, return...
if ( this._type === value )
{
return;
}
// Update the class name
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-type-' + this.type);
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-type-' + value);
// Store the value
this._type = value;
}
get type(): TreoMessageType
{
return this._type;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Subscribe to the service calls if only
// a name provided for the message box
if ( this.name )
{
// Subscribe to the dismiss calls
this._treoMessageService.onDismiss
.pipe(
filter((name) => this.name === name),
takeUntil(this._unsubscribeAll)
)
.subscribe(() => {
// Dismiss the message box
this.dismiss();
});
// Subscribe to the show calls
this._treoMessageService.onShow
.pipe(
filter((name) => this.name === name),
takeUntil(this._unsubscribeAll)
)
.subscribe(() => {
// Show the message box
this.show();
});
}
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Dismiss the message box
*/
dismiss(): void
{
// Return, if already dismissed
if ( this.dismissed )
{
return;
}
// Dismiss
this.dismissed = true;
// Execute the observable
this.afterDismissed.next(true);
// Notify the change detector
this._changeDetectorRef.markForCheck();
}
/**
* Show the dismissed message box
*/
show(): void
{
// Return, if not dismissed
if ( !this.dismissed )
{
return;
}
// Show
this.dismissed = false;
// Execute the observable
this.afterShown.next(true);
// Notify the change detector
this._changeDetectorRef.markForCheck();
}
}
@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { TreoMessageComponent } from '@treo/components/message/message.component';
@NgModule({
declarations: [
TreoMessageComponent
],
imports : [
CommonModule,
MatButtonModule,
MatIconModule
],
exports : [
TreoMessageComponent
]
})
export class TreoMessageModule
{
}
@@ -0,0 +1,81 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class TreoMessageService
{
// Private
private _onDismiss: BehaviorSubject<any>;
private _onShow: BehaviorSubject<any>;
/**
* Constructor
*/
constructor()
{
// Set the private defaults
this._onDismiss = new BehaviorSubject(null);
this._onShow = new BehaviorSubject(null);
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Getter for onDismiss
*/
get onDismiss(): Observable<any>
{
return this._onDismiss.asObservable();
}
/**
* Getter for onShow
*/
get onShow(): Observable<any>
{
return this._onShow.asObservable();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Dismiss the message box
*
* @param name
*/
dismiss(name: string): void
{
// Return, if the name is not provided
if ( !name )
{
return;
}
// Execute the observable
this._onDismiss.next(name);
}
/**
* Show the dismissed message box
*
* @param name
*/
show(name: string): void
{
// Return, if the name is not provided
if ( !name )
{
return;
}
// Execute the observable
this._onShow.next(name);
}
}
@@ -0,0 +1,2 @@
export type TreoMessageAppearance = 'border' | 'fill' | 'outline';
export type TreoMessageType = 'primary' | 'accent' | 'warn' | 'basic' | 'info' | 'success' | 'warning' | 'error';
@@ -0,0 +1,4 @@
export * from '@treo/components/message/message.component';
export * from '@treo/components/message/message.module';
export * from '@treo/components/message/message.service';
export * from '@treo/components/message/message.types';
@@ -0,0 +1,92 @@
<!-- Item wrapper -->
<div class="treo-horizontal-navigation-item-wrapper"
[class.treo-horizontal-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<!-- Item with an internal link -->
<div class="treo-horizontal-navigation-item"
*ngIf="item.link && !item.externalLink && !item.function && !item.disabled"
[routerLink]="[item.link]"
[routerLinkActive]="'treo-horizontal-navigation-item-active'"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item with an external link -->
<a class="treo-horizontal-navigation-item"
*ngIf="item.link && item.externalLink && !item.function && !item.disabled"
[href]="item.link">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with a function -->
<div class="treo-horizontal-navigation-item"
*ngIf="!item.link && item.function && !item.disabled"
(click)="item.function(item)">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item with an internal link and function -->
<div class="treo-horizontal-navigation-item"
*ngIf="item.link && !item.externalLink && item.function && !item.disabled"
[routerLink]="[item.link]"
[routerLinkActive]="'treo-horizontal-navigation-item-active'"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}"
(click)="item.function(item)">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item with an external link and function -->
<a class="treo-horizontal-navigation-item"
*ngIf="item.link && item.externalLink && item.function && !item.disabled"
[href]="item.link"
(click)="item.function(item)"
mat-menu-item>
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with a no link and no function -->
<div class="treo-horizontal-navigation-item"
*ngIf="!item.link && !item.function && !item.disabled">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item is disabled -->
<div class="treo-horizontal-navigation-item treo-horizontal-navigation-item-disabled"
*ngIf="item.disabled">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
</div>
<!-- Item template -->
<ng-template #itemTemplate>
<!-- Icon -->
<mat-icon class="treo-horizontal-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-horizontal-navigation-item-title-wrapper">
<div class="treo-horizontal-navigation-item-title">{{item.title}}</div>
<div class="treo-horizontal-navigation-item-subtitle text-hint"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-horizontal-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-horizontal-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-horizontal-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
</ng-template>
@@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-horizontal-navigation-basic-item',
templateUrl : './basic.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoHorizontalNavigationBasicItemComponent implements OnInit, OnDestroy
{
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,93 @@
<div *ngIf="!child"
[ngClass]="{'treo-horizontal-navigation-menu-active': trigger.menuOpen}"
[matMenuTriggerFor]="matMenu"
(onMenuOpen)="triggerChangeDetection()"
(onMenuClose)="triggerChangeDetection()"
#trigger="matMenuTrigger">
<ng-container *ngTemplateOutlet="itemTemplate; context: {$implicit: item}"></ng-container>
</div>
<mat-menu class="treo-horizontal-navigation-menu-panel"
[overlapTrigger]="false"
#matMenu="matMenu">
<ng-container *ngFor="let item of item.children">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Basic -->
<div class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'basic'"
mat-menu-item>
<treo-horizontal-navigation-basic-item [item]="item"
[name]="name"></treo-horizontal-navigation-basic-item>
</div>
<!-- Branch: aside, collapsable, group -->
<div class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'aside' || item.type === 'collapsable' || item.type === 'group'"
[matMenuTriggerFor]="branch.matMenu"
mat-menu-item>
<ng-container *ngTemplateOutlet="itemTemplate; context: {$implicit: item}"></ng-container>
<treo-horizontal-navigation-branch-item [child]="true"
[item]="item"
[name]="name"
#branch></treo-horizontal-navigation-branch-item>
</div>
<!-- Divider -->
<div class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'divider'"
mat-menu-item>
<treo-horizontal-navigation-divider-item [item]="item"
[name]="name"></treo-horizontal-navigation-divider-item>
</div>
</ng-container>
</ng-container>
</mat-menu>
<!-- Item template -->
<ng-template let-item
#itemTemplate>
<div class="treo-horizontal-navigation-item-wrapper"
[class.treo-horizontal-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<div class="treo-horizontal-navigation-item"
[ngClass]="{'treo-horizontal-navigation-item-disabled': item.disabled}">
<!-- Icon -->
<mat-icon class="treo-horizontal-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-horizontal-navigation-item-title-wrapper">
<div class="treo-horizontal-navigation-item-title">{{item.title}}</div>
<div class="treo-horizontal-navigation-item-subtitle text-hint"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-horizontal-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-horizontal-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-horizontal-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
</div>
</div>
</ng-template>
@@ -0,0 +1,99 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatMenu } from '@angular/material/menu';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-horizontal-navigation-branch-item',
templateUrl : './branch.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoHorizontalNavigationBranchItemComponent implements OnInit, OnDestroy
{
// Child
@Input()
child: boolean;
// Item
@Input()
item: TreoNavigationItem;
// Mat menu
@ViewChild('matMenu', {static: true})
matMenu: MatMenu;
// Name
@Input()
name: string;
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.child = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Trigger the change detection
*/
triggerChangeDetection(): void
{
// Mark for check
this._changeDetectorRef.markForCheck();
}
}
@@ -0,0 +1,2 @@
<!-- Divider -->
<div class="treo-horizontal-navigation-item-wrapper divider"></div>
@@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-horizontal-navigation-divider-item',
templateUrl : './divider.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoHorizontalNavigationDividerItemComponent implements OnInit, OnDestroy
{
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,2 @@
<!-- Spacer -->
<div class="treo-horizontal-navigation-item-wrapper"></div>
@@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-horizontal-navigation-spacer-item',
templateUrl : './spacer.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoHorizontalNavigationSpacerItemComponent implements OnInit, OnDestroy
{
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,30 @@
<div class="treo-horizontal-navigation-wrapper">
<ng-container *ngFor="let item of navigation">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Basic -->
<treo-horizontal-navigation-basic-item class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'basic'"
[item]="item"
[name]="name"></treo-horizontal-navigation-basic-item>
<!-- Branch: aside, collapsable, group -->
<treo-horizontal-navigation-branch-item class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'aside' || item.type === 'collapsable' || item.type === 'group'"
[item]="item"
[name]="name"></treo-horizontal-navigation-branch-item>
<!-- Spacer -->
<treo-horizontal-navigation-spacer-item class="treo-horizontal-navigation-menu-item"
*ngIf="item.type === 'spacer'"
[item]="item"
[name]="name"></treo-horizontal-navigation-spacer-item>
</ng-container>
</ng-container>
</div>
@@ -0,0 +1,234 @@
@import 'treo';
// Root navigation specific
treo-horizontal-navigation {
.treo-horizontal-navigation-wrapper {
display: flex;
align-items: center;
// Basic, Branch
treo-horizontal-navigation-basic-item,
treo-horizontal-navigation-branch-item {
.treo-horizontal-navigation-item-wrapper {
border-radius: 4px;
overflow: hidden;
.treo-horizontal-navigation-item {
padding: 0 16px;
cursor: pointer;
user-select: none;
.treo-horizontal-navigation-item-icon {
margin-right: 12px;
}
}
}
}
// Spacer
treo-horizontal-navigation-spacer-item {
margin: 12px 0;
}
}
}
// Menu panel specific
.treo-horizontal-navigation-menu-panel {
.treo-horizontal-navigation-menu-item {
height: auto;
min-height: 0;
line-height: normal;
white-space: normal;
// Basic, Branch
treo-horizontal-navigation-basic-item,
treo-horizontal-navigation-branch-item,
treo-horizontal-navigation-divider-item {
display: flex;
flex: 1 1 auto;
}
// Divider
treo-horizontal-navigation-divider-item {
margin: 8px -16px;
.treo-horizontal-navigation-item-wrapper {
height: 1px;
box-shadow: 0 1px 0 0;
}
}
}
}
// Navigation menu item common
.treo-horizontal-navigation-menu-item {
.treo-horizontal-navigation-item-wrapper {
width: 100%;
&.treo-horizontal-navigation-item-has-subtitle {
.treo-horizontal-navigation-item {
min-height: 56px;
}
}
.treo-horizontal-navigation-item {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
min-height: 48px;
width: 100%;
font-size: 13px;
font-weight: 500;
text-decoration: none;
.treo-horizontal-navigation-item-title-wrapper {
.treo-horizontal-navigation-item-subtitle {
font-size: 12px;
}
}
.treo-horizontal-navigation-item-badge {
margin-left: auto;
.treo-horizontal-navigation-item-badge-content {
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
white-space: nowrap;
width: 20px;
height: 20px;
border-radius: 50%;
// Rectangle
&.treo-horizontal-navigation-item-badge-style-rectangle {
width: auto;
min-width: 24px;
height: 20px;
line-height: normal;
padding: 0 6px;
border-radius: 4px;
}
// Rounded
&.treo-horizontal-navigation-item-badge-style-rounded {
width: auto;
min-width: 24px;
height: 20px;
line-height: normal;
padding: 0 10px;
border-radius: 12px;
}
// Simple
&.treo-horizontal-navigation-item-badge-style-simple {
width: auto;
font-size: 11px;
background-color: transparent !important;
}
}
}
}
}
}
// -----------------------------------------------------------------------------------------------------
// @ Theming
// -----------------------------------------------------------------------------------------------------
@include treo-theme {
$background: map-get($theme, background);
$primary: map-get($theme, primary);
$is-dark: map-get($theme, is-dark);
// Root navigation specific
treo-horizontal-navigation {
.treo-horizontal-navigation-wrapper {
// Basic, Branch
treo-horizontal-navigation-basic-item,
treo-horizontal-navigation-branch-item {
@include treo-breakpoint('gt-xs') {
&:hover {
.treo-horizontal-navigation-item-wrapper {
background: map-get($background, hover);
}
}
}
}
// Basic - When item active (current link)
treo-horizontal-navigation-basic-item {
.treo-horizontal-navigation-item-active {
.treo-horizontal-navigation-item-title {
color: map-get($primary, default) !important;
}
.treo-horizontal-navigation-item-subtitle {
@if ($is-dark) {
color: map-get($primary, 600) !important;
} @else {
color: map-get($primary, 400) !important;
}
}
.treo-horizontal-navigation-item-icon {
color: map-get($primary, default) !important;
}
}
}
// Branch - When menu open
treo-horizontal-navigation-branch-item {
.treo-horizontal-navigation-menu-active {
.treo-horizontal-navigation-item-wrapper {
background: map-get($background, hover);
}
}
}
}
}
// Navigation menu item common
.treo-horizontal-navigation-menu-item {
// Basic - When item active (current link)
treo-horizontal-navigation-basic-item {
.treo-horizontal-navigation-item-active {
.treo-horizontal-navigation-item-title {
color: map-get($primary, default) !important;
}
.treo-horizontal-navigation-item-subtitle {
@if ($is-dark) {
color: map-get($primary, 600) !important;
} @else {
color: map-get($primary, 400) !important;
}
}
.treo-horizontal-navigation-item-icon {
color: map-get($primary, default) !important;
}
}
}
}
}
@@ -0,0 +1,120 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { TreoAnimations } from '@treo/animations';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
@Component({
selector : 'treo-horizontal-navigation',
templateUrl : './horizontal.component.html',
styleUrls : ['./horizontal.component.scss'],
animations : TreoAnimations,
encapsulation : ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
exportAs : 'treoHorizontalNavigation'
})
export class TreoHorizontalNavigationComponent implements OnInit, OnDestroy
{
onRefreshed: BehaviorSubject<boolean | null>;
// Name
@Input()
name: string;
// Private
private _navigation: TreoNavigationItem[];
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.onRefreshed = new BehaviorSubject(null);
}
// -----------------------------------------------------------------------------------------------------
// @ Accessors
// -----------------------------------------------------------------------------------------------------
/**
* Setter & getter for data
*/
@Input()
set navigation(value: TreoNavigationItem[])
{
// Store the data
this._navigation = value;
// Mark for check
this._changeDetectorRef.markForCheck();
}
get navigation(): TreoNavigationItem[]
{
return this._navigation;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Register the navigation component
this._treoNavigationService.registerComponent(this.name, this);
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Deregister the navigation component from the registry
this._treoNavigationService.deregisterComponent(this.name);
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Refresh the component to apply the changes
*/
refresh(): void
{
// Mark for check
this._changeDetectorRef.markForCheck();
// Execute the observable
this.onRefreshed.next(true);
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}
@@ -0,0 +1 @@
export * from '@treo/components/navigation/public-api';
@@ -0,0 +1,55 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TreoScrollbarModule } from '@treo/directives/scrollbar/public-api';
import { TreoHorizontalNavigationBasicItemComponent } from '@treo/components/navigation/horizontal/components/basic/basic.component';
import { TreoHorizontalNavigationBranchItemComponent } from '@treo/components/navigation/horizontal/components/branch/branch.component';
import { TreoHorizontalNavigationDividerItemComponent } from '@treo/components/navigation/horizontal/components/divider/divider.component';
import { TreoHorizontalNavigationSpacerItemComponent } from '@treo/components/navigation/horizontal/components/spacer/spacer.component';
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
import { TreoVerticalNavigationAsideItemComponent } from '@treo/components/navigation/vertical/components/aside/aside.component';
import { TreoVerticalNavigationBasicItemComponent } from '@treo/components/navigation/vertical/components/basic/basic.component';
import { TreoVerticalNavigationCollapsableItemComponent } from '@treo/components/navigation/vertical/components/collapsable/collapsable.component';
import { TreoVerticalNavigationDividerItemComponent } from '@treo/components/navigation/vertical/components/divider/divider.component';
import { TreoVerticalNavigationGroupItemComponent } from '@treo/components/navigation/vertical/components/group/group.component';
import { TreoVerticalNavigationSpacerItemComponent } from '@treo/components/navigation/vertical/components/spacer/spacer.component';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
@NgModule({
declarations: [
TreoHorizontalNavigationBasicItemComponent,
TreoHorizontalNavigationBranchItemComponent,
TreoHorizontalNavigationDividerItemComponent,
TreoHorizontalNavigationSpacerItemComponent,
TreoHorizontalNavigationComponent,
TreoVerticalNavigationAsideItemComponent,
TreoVerticalNavigationBasicItemComponent,
TreoVerticalNavigationCollapsableItemComponent,
TreoVerticalNavigationDividerItemComponent,
TreoVerticalNavigationGroupItemComponent,
TreoVerticalNavigationSpacerItemComponent,
TreoVerticalNavigationComponent
],
imports : [
CommonModule,
RouterModule,
MatButtonModule,
MatDividerModule,
MatIconModule,
MatMenuModule,
MatTooltipModule,
TreoScrollbarModule
],
exports : [
TreoHorizontalNavigationComponent,
TreoVerticalNavigationComponent
]
})
export class TreoNavigationModule
{
}
@@ -0,0 +1,192 @@
import { Injectable } from '@angular/core';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Injectable({
providedIn: 'root'
})
export class TreoNavigationService
{
// Private
private _componentRegistry: Map<string, any>;
private _navigationStore: Map<string, TreoNavigationItem[]>;
/**
* Constructor
*/
constructor()
{
// Set the private defaults
this._componentRegistry = new Map<string, any>();
this._navigationStore = new Map<string, any>();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Register navigation component
*
* @param name
* @param component
*/
registerComponent(name: string, component: any): void
{
this._componentRegistry.set(name, component);
}
/**
* Deregister navigation component
*
* @param name
*/
deregisterComponent(name: string): void
{
this._componentRegistry.delete(name);
}
/**
* Get navigation component from the registry
*
* @param name
*/
getComponent(name: string): any
{
return this._componentRegistry.get(name);
}
/**
* Store the given navigation with the given key
*
* @param key
* @param navigation
*/
storeNavigation(key: string, navigation: TreoNavigationItem[]): void
{
// Add to the store
this._navigationStore.set(key, navigation);
}
/**
* Get navigation from storage by key
*
* @param key
* @returns {any}
*/
getNavigation(key: string): TreoNavigationItem[]
{
return this._navigationStore.get(key);
}
/**
* Delete the navigation from the storage
*
* @param key
*/
deleteNavigation(key: string): void
{
// Check if the navigation exists
if ( !this._navigationStore.has(key) )
{
console.warn(`Navigation with the key '${key}' does not exist in the store.`);
}
// Delete from the storage
this._navigationStore.delete(key);
}
/**
* Utility function that returns a flattened
* version of the given navigation array
*
* @param navigation
* @param flatNavigation
* @returns {TreoNavigationItem[]}
*/
getFlatNavigation(navigation: TreoNavigationItem[], flatNavigation: TreoNavigationItem[] = []): TreoNavigationItem[]
{
for ( const item of navigation )
{
if ( item.type === 'basic' )
{
flatNavigation.push(item);
continue;
}
if ( item.type === 'aside' || item.type === 'collapsable' || item.type === 'group' )
{
if ( item.children )
{
this.getFlatNavigation(item.children, flatNavigation);
}
}
}
return flatNavigation;
}
/**
* Utility function that returns the item
* with the given id from given navigation
*
* @param id
* @param navigation
*/
getItem(id: string, navigation: TreoNavigationItem[]): TreoNavigationItem | null
{
for ( const item of navigation )
{
if ( item.id === id )
{
return item;
}
if ( item.children )
{
const childItem = this.getItem(id, item.children);
if ( childItem )
{
return childItem;
}
}
}
return null;
}
/**
* Utility function that returns the item's parent
* with the given id from given navigation
*
* @param id
* @param navigation
* @param parent
*/
getItemParent(
id: string,
navigation: TreoNavigationItem[],
parent: TreoNavigationItem[] | TreoNavigationItem
): TreoNavigationItem[] | TreoNavigationItem | null
{
for ( const item of navigation )
{
if ( item.id === id )
{
return parent;
}
if ( item.children )
{
const childItem = this.getItemParent(id, item.children, item);
if ( childItem )
{
return childItem;
}
}
}
return null;
}
}
@@ -0,0 +1,28 @@
export interface TreoNavigationItem
{
id?: string;
title?: string;
subtitle?: string;
type: 'aside' | 'basic' | 'collapsable' | 'divider' | 'group' | 'spacer';
hidden?: (item: TreoNavigationItem) => boolean;
disabled?: boolean;
link?: string;
externalLink?: boolean;
exactMatch?: boolean;
function?: (item: TreoNavigationItem) => void;
classes?: string;
icon?: string;
iconClasses?: string;
badge?: {
title?: string;
style?: 'rectangle' | 'rounded' | 'simple',
background?: string;
color?: string;
};
children?: TreoNavigationItem[];
meta?: any;
}
export type TreoVerticalNavigationAppearance = string;
export type TreoVerticalNavigationMode = 'over' | 'side';
export type TreoVerticalNavigationPosition = 'left' | 'right';
@@ -0,0 +1,5 @@
export * from '@treo/components/navigation/horizontal/horizontal.component';
export * from '@treo/components/navigation/vertical/vertical.component';
export * from '@treo/components/navigation/navigation.module';
export * from '@treo/components/navigation/navigation.service';
export * from '@treo/components/navigation/navigation.types';
@@ -0,0 +1,83 @@
<div class="treo-vertical-navigation-item-wrapper"
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<div class="treo-vertical-navigation-item"
[ngClass]="{'treo-vertical-navigation-item-active': active, 'treo-vertical-navigation-item-disabled': item.disabled}">
<!-- Icon -->
<mat-icon class="treo-vertical-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-vertical-navigation-item-title-wrapper">
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
<div class="treo-vertical-navigation-item-subtitle"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-vertical-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-vertical-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
</div>
</div>
<ng-container *ngIf="!skipChildren">
<div class="treo-vertical-navigation-item-children">
<ng-container *ngFor="let item of item.children; trackBy: trackByFn">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Basic -->
<treo-vertical-navigation-basic-item *ngIf="item.type === 'basic'"
[item]="item"
[name]="name"></treo-vertical-navigation-basic-item>
<!-- Collapsable -->
<treo-vertical-navigation-collapsable-item *ngIf="item.type === 'collapsable'"
[item]="item"
[name]="name"
[autoCollapse]="autoCollapse"></treo-vertical-navigation-collapsable-item>
<!-- Divider -->
<treo-vertical-navigation-divider-item *ngIf="item.type === 'divider'"
[item]="item"
[name]="name"></treo-vertical-navigation-divider-item>
<!-- Group -->
<treo-vertical-navigation-group-item *ngIf="item.type === 'group'"
[item]="item"
[name]="name"></treo-vertical-navigation-group-item>
<!-- Spacer -->
<treo-vertical-navigation-spacer-item *ngIf="item.type === 'spacer'"
[item]="item"
[name]="name"></treo-vertical-navigation-spacer-item>
</ng-container>
</ng-container>
</div>
</ng-container>
@@ -0,0 +1,104 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-vertical-navigation-aside-item',
templateUrl : './aside.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoVerticalNavigationAsideItemComponent implements OnInit, OnDestroy
{
// Active
@Input()
active: boolean;
// Auto collapse
@Input()
autoCollapse: boolean;
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Skip children
@Input()
skipChildren: boolean;
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.skipChildren = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoVerticalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}
@@ -0,0 +1,91 @@
<!-- Item wrapper -->
<div class="treo-vertical-navigation-item-wrapper"
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<!-- Item with an internal link -->
<a class="treo-vertical-navigation-item"
*ngIf="item.link && !item.externalLink && !item.function && !item.disabled"
[routerLink]="[item.link]"
[routerLinkActive]="'treo-vertical-navigation-item-active'"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with an external link -->
<a class="treo-vertical-navigation-item"
*ngIf="item.link && item.externalLink && !item.function && !item.disabled"
[href]="item.link">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with a function -->
<div class="treo-vertical-navigation-item"
*ngIf="!item.link && item.function && !item.disabled"
(click)="item.function(item)">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item with an internal link and function -->
<a class="treo-vertical-navigation-item"
*ngIf="item.link && !item.externalLink && item.function && !item.disabled"
[routerLink]="[item.link]"
[routerLinkActive]="'treo-vertical-navigation-item-active'"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}"
(click)="item.function(item)">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with an external link and function -->
<a class="treo-vertical-navigation-item"
*ngIf="item.link && item.externalLink && item.function && !item.disabled"
[href]="item.link"
(click)="item.function(item)">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</a>
<!-- Item with a no link and no function -->
<div class="treo-vertical-navigation-item"
*ngIf="!item.link && !item.function && !item.disabled">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
<!-- Item is disabled -->
<div class="treo-vertical-navigation-item treo-vertical-navigation-item-disabled"
*ngIf="item.disabled">
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
</div>
</div>
<!-- Item template -->
<ng-template #itemTemplate>
<!-- Icon -->
<mat-icon class="treo-vertical-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-vertical-navigation-item-title-wrapper">
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
<div class="treo-vertical-navigation-item-subtitle"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-vertical-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-vertical-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
</ng-template>
@@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-vertical-navigation-basic-item',
templateUrl : './basic.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoVerticalNavigationBasicItemComponent implements OnInit, OnDestroy
{
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoVerticalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,85 @@
<div class="treo-vertical-navigation-item-wrapper"
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<div class="treo-vertical-navigation-item"
[ngClass]="{'treo-vertical-navigation-item-disabled': item.disabled}"
(click)="toggleCollapsable()">
<!-- Icon -->
<mat-icon class="treo-vertical-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-vertical-navigation-item-title-wrapper">
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
<div class="treo-vertical-navigation-item-subtitle"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-vertical-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-vertical-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
<!-- Arrow -->
<mat-icon class="treo-vertical-navigation-item-arrow icon-size-16"
[svgIcon]="'heroicons_solid:cheveron-right'"></mat-icon>
</div>
</div>
<div class="treo-vertical-navigation-item-children"
*ngIf="!isCollapsed"
@expandCollapse>
<ng-container *ngFor="let item of item.children; trackBy: trackByFn">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Basic -->
<treo-vertical-navigation-basic-item *ngIf="item.type === 'basic'"
[item]="item"
[name]="name"></treo-vertical-navigation-basic-item>
<!-- Collapsable -->
<treo-vertical-navigation-collapsable-item *ngIf="item.type === 'collapsable'"
[item]="item"
[name]="name"
[autoCollapse]="autoCollapse"></treo-vertical-navigation-collapsable-item>
<!-- Divider -->
<treo-vertical-navigation-divider-item *ngIf="item.type === 'divider'"
[item]="item"
[name]="name"></treo-vertical-navigation-divider-item>
<!-- Group -->
<treo-vertical-navigation-group-item *ngIf="item.type === 'group'"
[item]="item"
[name]="name"></treo-vertical-navigation-group-item>
<!-- Spacer -->
<treo-vertical-navigation-spacer-item *ngIf="item.type === 'spacer'"
[item]="item"
[name]="name"></treo-vertical-navigation-spacer-item>
</ng-container>
</ng-container>
</div>
@@ -0,0 +1,363 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { TreoAnimations } from '@treo/animations';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-vertical-navigation-collapsable-item',
templateUrl : './collapsable.component.html',
styles : [],
animations : TreoAnimations,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoVerticalNavigationCollapsableItemComponent implements OnInit, OnDestroy
{
// Auto collapse
@Input()
autoCollapse: boolean;
// Item
@Input()
item: TreoNavigationItem;
// Collapsed
@HostBinding('class.treo-vertical-navigation-item-collapsed')
isCollapsed: boolean;
// Expanded
@HostBinding('class.treo-vertical-navigation-item-expanded')
isExpanded: boolean;
// Name
@Input()
name: string;
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
* @param {Router} _router
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef,
private _router: Router
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
// Set the defaults
this.isCollapsed = true;
this.isExpanded = false;
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// If the item has a children that has a matching url with the current url, expand...
if ( this._hasCurrentUrlInChildren(this.item, this._router.url) )
{
this.expand();
}
// Otherwise...
else
{
// If the autoCollapse is on, collapse...
if ( this.autoCollapse )
{
this.collapse();
}
}
// Listen for the onCollapsableItemCollapsed from the service
this._treoVerticalNavigationComponent.onCollapsableItemCollapsed
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((collapsedItem) => {
// Check if the collapsed item is null
if ( collapsedItem === null )
{
return;
}
// Collapse if this is a children of the collapsed item
if ( this._isChildrenOf(collapsedItem, this.item) )
{
this.collapse();
}
});
// Listen for the onCollapsableItemExpanded from the service if the autoCollapse is on
if ( this.autoCollapse )
{
this._treoVerticalNavigationComponent.onCollapsableItemExpanded
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((expandedItem) => {
// Check if the expanded item is null
if ( expandedItem === null )
{
return;
}
// Check if this is a parent of the expanded item
if ( this._isChildrenOf(this.item, expandedItem) )
{
return;
}
// Check if this has a children with a matching url with the current active url
if ( this._hasCurrentUrlInChildren(this.item, this._router.url) )
{
return;
}
// Check if this is the expanded item
if ( this.item === expandedItem )
{
return;
}
// If none of the above conditions are matched, collapse this item
this.collapse();
});
}
// Attach a listener to the NavigationEnd event
this._router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this._unsubscribeAll)
)
.subscribe((event: NavigationEnd) => {
// If the item has a children that has a matching url with the current url, expand...
if ( this._hasCurrentUrlInChildren(this.item, event.urlAfterRedirects) )
{
this.expand();
}
// Otherwise...
else
{
// If the autoCollapse is on, collapse...
if ( this.autoCollapse )
{
this.collapse();
}
}
});
// Subscribe to onRefreshed on the navigation component
this._treoVerticalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
// -----------------------------------------------------------------------------------------------------
// @ Private methods
// -----------------------------------------------------------------------------------------------------
/**
* Check if the given item has the given url
* in one of its children
*
* @param item
* @param url
* @private
*/
private _hasCurrentUrlInChildren(item, url): boolean
{
const children = item.children;
if ( !children )
{
return false;
}
for ( const child of children )
{
if ( child.children )
{
if ( this._hasCurrentUrlInChildren(child, url) )
{
return true;
}
}
// Check if the item's link is the exact same of the
// current url
if ( child.link === url )
{
return true;
}
// If exactMatch is not set for the item, also check
// if the current url starts with the item's link and
// continues with a question mark, a pound sign or a
// slash
if ( !child.exactMatch && (child.link === url || url.startsWith(child.link + '?') || url.startsWith(child.link + '#') || url.startsWith(child.link + '/')) )
{
return true;
}
}
return false;
}
/**
* Check if this is a children
* of the given item
*
* @param parent
* @param item
* @return {boolean}
* @private
*/
private _isChildrenOf(parent, item): boolean
{
const children = parent.children;
if ( !children )
{
return false;
}
if ( children.indexOf(item) > -1 )
{
return true;
}
for ( const child of children )
{
if ( child.children )
{
if ( this._isChildrenOf(child, item) )
{
return true;
}
}
}
return false;
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Collapse
*/
collapse(): void
{
// Return if the item is disabled
if ( this.item.disabled )
{
return;
}
// Return if the item is already collapsed
if ( this.isCollapsed )
{
return;
}
// Collapse it
this.isCollapsed = true;
this.isExpanded = false;
// Mark for check
this._changeDetectorRef.markForCheck();
// Execute the observable
this._treoVerticalNavigationComponent.onCollapsableItemCollapsed.next(this.item);
}
/**
* Expand
*/
expand(): void
{
// Return if the item is disabled
if ( this.item.disabled )
{
return;
}
// Return if the item is already expanded
if ( !this.isCollapsed )
{
return;
}
// Expand it
this.isCollapsed = false;
this.isExpanded = true;
// Mark for check
this._changeDetectorRef.markForCheck();
// Execute the observable
this._treoVerticalNavigationComponent.onCollapsableItemExpanded.next(this.item);
}
/**
* Toggle collapsable
*/
toggleCollapsable(): void
{
// Toggle collapse/expand
if ( this.isCollapsed )
{
this.expand();
}
else
{
this.collapse();
}
}
/**
* Track by function for ngFor loops
*
* @param index
* @param item
*/
trackByFn(index: number, item: any): any
{
return item.id || index;
}
}
@@ -0,0 +1,2 @@
<!-- Divider -->
<div class="treo-vertical-navigation-item-wrapper divider"></div>
@@ -0,0 +1,74 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
@Component({
selector : 'treo-vertical-navigation-divider-item',
templateUrl : './divider.component.html',
styles : [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreoVerticalNavigationDividerItemComponent implements OnInit, OnDestroy
{
// Item
@Input()
item: TreoNavigationItem;
// Name
@Input()
name: string;
// Private
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
private _unsubscribeAll: Subject<any>;
/**
* Constructor
*
* @param {TreoNavigationService} _treoNavigationService
* @param {ChangeDetectorRef} _changeDetectorRef
*/
constructor(
private _treoNavigationService: TreoNavigationService,
private _changeDetectorRef: ChangeDetectorRef
)
{
// Set the private defaults
this._unsubscribeAll = new Subject();
}
// -----------------------------------------------------------------------------------------------------
// @ Lifecycle hooks
// -----------------------------------------------------------------------------------------------------
/**
* On init
*/
ngOnInit(): void
{
// Get the parent navigation component
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
// Subscribe to onRefreshed on the navigation component
this._treoVerticalNavigationComponent.onRefreshed.pipe(
takeUntil(this._unsubscribeAll)
).subscribe(() => {
// Mark for check
this._changeDetectorRef.markForCheck();
});
}
/**
* On destroy
*/
ngOnDestroy(): void
{
// Unsubscribe from all subscriptions
this._unsubscribeAll.next();
this._unsubscribeAll.complete();
}
}
@@ -0,0 +1,74 @@
<!-- Item wrapper -->
<div class="treo-vertical-navigation-item-wrapper"
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
[ngClass]="item.classes">
<div class="treo-vertical-navigation-item">
<!-- Icon -->
<mat-icon class="treo-vertical-navigation-item-icon"
[ngClass]="item.iconClasses"
*ngIf="item.icon"
[svgIcon]="item.icon"></mat-icon>
<!-- Title & Subtitle -->
<div class="treo-vertical-navigation-item-title-wrapper">
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
<div class="treo-vertical-navigation-item-subtitle"
*ngIf="item.subtitle">
{{item.subtitle}}
</div>
</div>
<!-- Badge -->
<div class="treo-vertical-navigation-item-badge"
*ngIf="item.badge">
<div class="treo-vertical-navigation-item-badge-content"
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
{{item.badge.title}}
</div>
</div>
</div>
</div>
<ng-container *ngFor="let item of item.children; trackBy: trackByFn">
<!-- Skip the hidden items -->
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
<!-- Basic -->
<treo-vertical-navigation-basic-item *ngIf="item.type === 'basic'"
[item]="item"
[name]="name"></treo-vertical-navigation-basic-item>
<!-- Collapsable -->
<treo-vertical-navigation-collapsable-item *ngIf="item.type === 'collapsable'"
[item]="item"
[name]="name"
[autoCollapse]="autoCollapse"></treo-vertical-navigation-collapsable-item>
<!-- Divider -->
<treo-vertical-navigation-divider-item *ngIf="item.type === 'divider'"
[item]="item"
[name]="name"></treo-vertical-navigation-divider-item>
<!-- Group -->
<treo-vertical-navigation-group-item *ngIf="item.type === 'group'"
[item]="item"
[name]="name"></treo-vertical-navigation-group-item>
<!-- Spacer -->
<treo-vertical-navigation-spacer-item *ngIf="item.type === 'spacer'"
[item]="item"
[name]="name"></treo-vertical-navigation-spacer-item>
</ng-container>
</ng-container>

Some files were not shown because too many files have changed in this diff Show More