commit
8482272d45
@ -0,0 +1,58 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# CMake
|
||||
cmake-build-debug/
|
||||
cmake-build-release/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
|
||||
scrutiny.db
|
||||
/dist/
|
@ -0,0 +1,42 @@
|
||||
# Contributing
|
||||
|
||||
There are multiple ways to develop on the scrutiny codebase locally. The two most popular are:
|
||||
- Docker Development Container - only requires docker
|
||||
- Run Components Locally - requires smartmontools, golang & nodejs installed locally
|
||||
|
||||
## Docker Development
|
||||
```
|
||||
docker build . -t analogj/scrutiny
|
||||
docker run -it --rm -p 9090:8080 -v /run:/run -v /dev/disk:/dev/disk --privileged analogj/scrutiny
|
||||
/scrutiny/bin/scrutiny-collector-metrics run
|
||||
```
|
||||
|
||||
|
||||
## Local Development
|
||||
|
||||
### Frontend
|
||||
The frontend is written in Angular.
|
||||
If you're working on the frontend and can use mocked data rather than a real backend, you can use
|
||||
```
|
||||
cd webapp/frontend && ng serve
|
||||
```
|
||||
|
||||
However, if you need to also run the backend, and use real data, you'll need to run the following command:
|
||||
```
|
||||
cd webapp/frontend && ng build --watch --output-path=../../dist --deploy-url="/web/" --base-href="/web/" --prod
|
||||
```
|
||||
|
||||
> Note: if you do not add `--prod` flag, app will display mocked data for api calls.
|
||||
|
||||
### Backend
|
||||
```
|
||||
go run webapp/backend/cmd/scrutiny/scrutiny.go start --config ./example.scrutiny.yaml
|
||||
```
|
||||
Now visit http://localhost:8080
|
||||
|
||||
|
||||
### Collector
|
||||
```
|
||||
brew install smartmontools
|
||||
go run collector/cmd/collector-metrics/collector-metrics.go run --debug
|
||||
```
|
@ -0,0 +1,55 @@
|
||||
########
|
||||
FROM golang:1.14.4-buster as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
COPY . /go/src/github.com/analogj/scrutiny
|
||||
|
||||
RUN go mod vendor && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go && \
|
||||
go build -o scrutiny-collector-selftest collector/cmd/collector-selftest/collector-selftest.go && \
|
||||
go build -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
|
||||
|
||||
########
|
||||
FROM node:lts-slim as frontendbuild
|
||||
|
||||
#reduce logging, disable angular-cli analytics for ci environment
|
||||
ENV NPM_CONFIG_LOGLEVEL=warn NG_CLI_ANALYTICS=false
|
||||
|
||||
WORKDIR /scrutiny/src
|
||||
COPY ./webapp/frontend /scrutiny/src
|
||||
|
||||
RUN npm install -g @angular/cli@9.1.4 && \
|
||||
mkdir -p /scrutiny/dist && \
|
||||
npm install && \
|
||||
ng build --output-path=/scrutiny/dist --deploy-url="/web/" --base-href="/web/" --prod
|
||||
|
||||
|
||||
########
|
||||
FROM ubuntu:bionic as runtime
|
||||
EXPOSE 8080
|
||||
WORKDIR /scrutiny
|
||||
ENV PATH="/scrutiny/bin:${PATH}"
|
||||
|
||||
ADD https://github.com/dshearer/jobber/releases/download/v1.4.4/jobber_1.4.4-1_amd64.deb /tmp/
|
||||
RUN apt install /tmp/jobber_1.4.4-1_amd64.deb
|
||||
|
||||
RUN apt-get update && apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1
|
||||
|
||||
ADD https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-amd64.tar.gz /tmp/
|
||||
RUN tar xzf /tmp/s6-overlay-amd64.tar.gz -C /
|
||||
COPY /rootfs /
|
||||
|
||||
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /scrutiny/bin/
|
||||
COPY --from=frontendbuild /scrutiny/dist /scrutiny/web
|
||||
RUN chmod +x /scrutiny/bin/scrutiny && \
|
||||
chmod +x /scrutiny/bin/scrutiny-collector-selftest && \
|
||||
chmod +x /scrutiny/bin/scrutiny-collector-metrics && \
|
||||
mkdir -p /scrutiny/web && \
|
||||
mkdir -p /scrutiny/config && \
|
||||
mkdir -p /scrutiny/jobber
|
||||
|
||||
CMD ["/init"]
|
@ -0,0 +1,36 @@
|
||||
########
|
||||
FROM golang:1.14.4-buster as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
COPY . /go/src/github.com/analogj/scrutiny
|
||||
|
||||
RUN go mod vendor && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-selftest collector/cmd/collector-selftest/collector-selftest.go && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny-collector-metrics collector/cmd/collector-metrics/collector-metrics.go
|
||||
|
||||
########
|
||||
FROM ubuntu:bionic as runtime
|
||||
EXPOSE 8080
|
||||
WORKDIR /scrutiny
|
||||
ENV PATH="/scrutiny/bin:${PATH}"
|
||||
|
||||
ADD https://github.com/dshearer/jobber/releases/download/v1.4.4/jobber_1.4.4-1_amd64.deb /tmp/
|
||||
RUN apt install /tmp/jobber_1.4.4-1_amd64.deb
|
||||
|
||||
RUN apt-get update && apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1
|
||||
|
||||
ADD https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-amd64.tar.gz /tmp/
|
||||
RUN tar xzf /tmp/s6-overlay-amd64.tar.gz -C / && \
|
||||
mkdir -p /rootfs/etc/services.d/jobber
|
||||
|
||||
COPY /rootfs/etc/services.d/jobber /etc/services.d/jobber
|
||||
COPY /rootfs/scrutiny /scrutiny
|
||||
|
||||
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /scrutiny/bin/
|
||||
RUN chmod +x /scrutiny/bin/scrutiny-collector-selftest && \
|
||||
chmod +x /scrutiny/bin/scrutiny-collector-metrics
|
||||
|
||||
CMD ["/init"]
|
@ -0,0 +1,53 @@
|
||||
########
|
||||
FROM golang:1.14.4-buster as backendbuild
|
||||
|
||||
WORKDIR /go/src/github.com/analogj/scrutiny
|
||||
|
||||
COPY . /go/src/github.com/analogj/scrutiny
|
||||
|
||||
RUN go mod vendor && \
|
||||
go build -ldflags '-w -extldflags "-static"' -o scrutiny webapp/backend/cmd/scrutiny/scrutiny.go
|
||||
|
||||
########
|
||||
FROM node:lts-slim as frontendbuild
|
||||
|
||||
#reduce logging, disable angular-cli analytics for ci environment
|
||||
ENV NPM_CONFIG_LOGLEVEL=warn NG_CLI_ANALYTICS=false
|
||||
|
||||
WORKDIR /scrutiny/src
|
||||
COPY ./webapp/frontend /scrutiny/src
|
||||
|
||||
RUN npm install -g @angular/cli@9.1.4 && \
|
||||
mkdir -p /scrutiny/dist && \
|
||||
npm install && \
|
||||
ng build --output-path=/scrutiny/dist --deploy-url="/web/" --base-href="/web/" --prod
|
||||
|
||||
|
||||
########
|
||||
FROM ubuntu:bionic as runtime
|
||||
EXPOSE 8080
|
||||
WORKDIR /scrutiny
|
||||
ENV PATH="/scrutiny/bin:${PATH}"
|
||||
|
||||
ADD https://github.com/dshearer/jobber/releases/download/v1.4.4/jobber_1.4.4-1_amd64.deb /tmp/
|
||||
RUN apt install /tmp/jobber_1.4.4-1_amd64.deb
|
||||
|
||||
RUN apt-get update && apt-get install -y smartmontools=7.0-0ubuntu1~ubuntu18.04.1
|
||||
|
||||
ADD https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-amd64.tar.gz /tmp/
|
||||
RUN tar xzf /tmp/s6-overlay-amd64.tar.gz -C /
|
||||
COPY /rootfs /
|
||||
|
||||
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny /scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-selftest /scrutiny/bin/
|
||||
COPY --from=backendbuild /go/src/github.com/analogj/scrutiny/scrutiny-collector-metrics /scrutiny/bin/
|
||||
COPY --from=frontendbuild /scrutiny/dist /scrutiny/web
|
||||
RUN chmod +x /scrutiny/bin/scrutiny && \
|
||||
chmod +x /scrutiny/bin/scrutiny-collector-selftest && \
|
||||
chmod +x /scrutiny/bin/scrutiny-collector-metrics && \
|
||||
mkdir -p /scrutiny/web && \
|
||||
mkdir -p /scrutiny/config && \
|
||||
mkdir -p /scrutiny/jobber
|
||||
|
||||
CMD ["/init"]
|
@ -0,0 +1,92 @@
|
||||
# scrutiny
|
||||
WebUI for smartd S.M.A.R.T monitoring
|
||||
|
||||
[![](docs/dashboard.png)](https://imgur.com/a/5k8qMzS)
|
||||
|
||||
# Introduction
|
||||
|
||||
If you run a server with more than a couple of hard drives, you're probably already familiar with S.M.A.R.T and the `smartd` daemon. If not, it's an incredible open source project described as the following:
|
||||
|
||||
> smartd is a daemon that monitors the Self-Monitoring, Analysis and Reporting Technology (SMART) system built into many ATA, IDE and SCSI-3 hard drives. The purpose of SMART is to monitor the reliability of the hard drive and predict drive failures, and to carry out different types of drive self-tests.
|
||||
|
||||
Theses S.M.A.R.T hard drive self-tests can help you detect and replace failing hard drives before they cause permanent data loss. However, there's a couple issues with `smartd`:
|
||||
|
||||
- There are more than a hundred S.M.A.R.T attributes, however `smartd` does not differentiate between critical and informational metrics
|
||||
- `smartd` does not record S.M.A.R.T attribute history, so it can be hard to determine if an attribute is degrading slowly over time.
|
||||
- S.M.A.R.T attribute thresholds are set by the manufacturer. In some cases these thresholds are unset, or are so high that they can only be used to confirm a failed drive, rather than detecting a drive about to fail.
|
||||
- `smartd` is a command line only tool. For head-less servers a web UI would be more valuable.
|
||||
|
||||
**Scrutiny is a Hard Drive Health Dashboard & Monitoring solution, merging manufacturer provided S.M.A.R.T metrics with real-world failure rates.**
|
||||
|
||||
# Features
|
||||
|
||||
Scrutiny is a simple but focused application, with a couple of core features:
|
||||
|
||||
- Web UI Dashboard - focused on Critical metrics
|
||||
- `smartd` integration (no re-inventing the wheel)
|
||||
- Auto-detection of all connected hard-drives
|
||||
- S.M.A.R.T metric tracking for historical trends
|
||||
- Customized thresholds using real world failure rates
|
||||
- Temperature tracking
|
||||
- Provided as an all-in-one Docker image (but can be installed manually)
|
||||
- (Future) Configurable Alerting/Notifications via Webhooks
|
||||
- (Future) Hard Drive performance testing & tracking
|
||||
|
||||
# Getting Started
|
||||
|
||||
## Docker
|
||||
|
||||
If you're using Docker, getting started is as simple as running the following command:
|
||||
|
||||
```bash
|
||||
|
||||
docker run -it --rm -p 8080:8080 \
|
||||
-v /run:/run \
|
||||
-v /dev/disk:/dev/disk \
|
||||
--name scrutiny \
|
||||
--privileged analogj/scrutiny
|
||||
```
|
||||
|
||||
- `/run` and `/dev/disk` are necessary to provide the Scrutiny application metadata about your drives
|
||||
- `--privileged` is required to ensure that your hard disk devices are accessible within the container (this will be changed in a future release)
|
||||
- `analogj/scrutiny` is a omnibus image, containing both the webapp server (frontend & api) as well as the S.M.A.R.T metric collector. (dedicated images will be available in a future release)
|
||||
- If you do not have access to the `analogj/scrutiny` docker image, please contact me using the email address in my profile: [@analogj](https://github.com/AnalogJ/) Please include your Github username and when you sponsored me. (eventually both images and source code will be open sourced)
|
||||
|
||||
## Usage
|
||||
|
||||
Once scrutiny is running, you can open your browser to `http://localhost:8080` and take a look at the dashboard.
|
||||
|
||||
Initially it will be empty, however after the first collector run, you'll be greeted with a list of all your hard drives and their current smart status.
|
||||
|
||||
The collector is configured to run once a day, but you can trigger it manually by running the following command
|
||||
|
||||
```
|
||||
docker exec scrutiny /scrutiny/bin/scrutiny-collector-metrics run
|
||||
```
|
||||
|
||||
# Configuration
|
||||
We support a global YAML configuration file that must be located at /scrutiny/config/scrutiny.yaml
|
||||
|
||||
Check the [example.scrutiny.yml](example.scrutiny.yaml) file for a fully commented version.
|
||||
|
||||
# Contributing
|
||||
|
||||
Please see the [CONTRIBUTING.md](CONTRIBUTING.md) for instructions for how to develop and contribute to the scrutiny codebase.
|
||||
|
||||
Work your magic and then submit a pull request. We love pull requests!
|
||||
|
||||
If you find the documentation lacking, help us out and update this README.md. If you don't have the time to work on Scrutiny, but found something we should know about, please submit an issue.
|
||||
|
||||
# Versioning
|
||||
|
||||
We use SemVer for versioning. For the versions available, see the tags on this repository.
|
||||
|
||||
# Authors
|
||||
|
||||
Jason Kulatunga - Initial Development - @AnalogJ
|
||||
|
||||
# License
|
||||
|
||||
All Rights Reserved
|
||||
|
||||
> Note: This license will change once certain sponsorship conditions are met. Please see the [reddit announcement post](https://www.reddit.com/r/selfhosted/comments/icreui/scrutiny_hard_drive_smart_monitoring_historical/) for more information.
|
@ -0,0 +1,9 @@
|
||||
|
||||
# Gorm
|
||||
- https://www.reddit.com/r/golang/comments/exmwos/golang_gorm_preload_with_last/
|
||||
- https://blog.depado.eu/post/gorm-gotchas
|
||||
-
|
||||
|
||||
|
||||
# Smart Data
|
||||
- https://kb.acronis.com/content/9123
|
@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/collector/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
"log"
|
||||
"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() {
|
||||
|
||||
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-collector-metrics",
|
||||
Usage: "smartctl data collector for scrutiny",
|
||||
Version: version.VERSION,
|
||||
Compiled: time.Now(),
|
||||
Authors: []*cli.Author{
|
||||
{
|
||||
Name: "Jason Kulatunga",
|
||||
Email: "jason@thesparktree.com",
|
||||
},
|
||||
},
|
||||
Before: func(c *cli.Context) error {
|
||||
|
||||
collectorMetrics := "AnalogJ/scrutiny/metrics"
|
||||
|
||||
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 := collectorMetrics + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorMetrics))
|
||||
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||
`
|
||||
___ ___ ____ __ __ ____ ____ _ _ _ _
|
||||
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
|
||||
\__ \( (__ ) / )(__)( )( _)(_ ) ( \ /
|
||||
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
|
||||
%s
|
||||
|
||||
`), subtitle))
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "run",
|
||||
Usage: "Run the scrutiny smartctl metrics collector",
|
||||
Action: func(c *cli.Context) error {
|
||||
|
||||
collectorLogger := logrus.WithFields(logrus.Fields{
|
||||
"type": "metrics",
|
||||
})
|
||||
|
||||
if c.Bool("debug") {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
} else {
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
metricCollector, err := collector.CreateMetricsCollector(
|
||||
collectorLogger,
|
||||
c.String("api-endpoint"),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return metricCollector.Run()
|
||||
},
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "api-endpoint",
|
||||
Usage: "The api server endpoint",
|
||||
Value: "http://localhost:8080",
|
||||
},
|
||||
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Enable debug logging",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(color.HiRedString("ERROR: %v", err))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/collector"
|
||||
"github.com/analogj/scrutiny/collector/pkg/version"
|
||||
"github.com/sirupsen/logrus"
|
||||
"log"
|
||||
"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() {
|
||||
|
||||
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-collector-selftest",
|
||||
Usage: "smartctl self-test data collector for scrutiny",
|
||||
Version: version.VERSION,
|
||||
Compiled: time.Now(),
|
||||
Authors: []*cli.Author{
|
||||
{
|
||||
Name: "Jason Kulatunga",
|
||||
Email: "jason@thesparktree.com",
|
||||
},
|
||||
},
|
||||
Before: func(c *cli.Context) error {
|
||||
|
||||
collectorSelfTest := "AnalogJ/scrutiny/selftest"
|
||||
|
||||
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 := collectorSelfTest + utils.LeftPad2Len(versionInfo, " ", 65-len(collectorSelfTest))
|
||||
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||
`
|
||||
___ ___ ____ __ __ ____ ____ _ _ _ _
|
||||
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
|
||||
\__ \( (__ ) / )(__)( )( _)(_ ) ( \ /
|
||||
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
|
||||
%s
|
||||
|
||||
`), subtitle))
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "run",
|
||||
Usage: "Run the scrutiny self-test data collector",
|
||||
Action: func(c *cli.Context) error {
|
||||
|
||||
collectorLogger := logrus.WithFields(logrus.Fields{
|
||||
"type": "selftest",
|
||||
})
|
||||
|
||||
if c.Bool("debug") {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
} else {
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
stCollector, err := collector.CreateSelfTestCollector(
|
||||
collectorLogger,
|
||||
c.String("api-endpoint"),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return stCollector.Run()
|
||||
},
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "api-endpoint",
|
||||
Usage: "The api server endpoint",
|
||||
Value: "http://localhost:8080",
|
||||
},
|
||||
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Enable debug logging",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(color.HiRedString("ERROR: %v", err))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
)
|
||||
|
||||
type BaseCollector struct{}
|
||||
|
||||
func (c *BaseCollector) getJson(url string, target interface{}) error {
|
||||
|
||||
r, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
return json.NewDecoder(r.Body).Decode(target)
|
||||
}
|
||||
|
||||
func (c *BaseCollector) execCmd(cmdName string, cmdArgs []string, workingDir string, environ []string) (string, error) {
|
||||
|
||||
cmd := exec.Command(cmdName, cmdArgs...)
|
||||
var stdBuffer bytes.Buffer
|
||||
mw := io.MultiWriter(os.Stdout, &stdBuffer)
|
||||
|
||||
cmd.Stdout = mw
|
||||
cmd.Stderr = mw
|
||||
|
||||
if environ != nil {
|
||||
cmd.Env = environ
|
||||
}
|
||||
if workingDir != "" && path.IsAbs(workingDir) {
|
||||
cmd.Dir = workingDir
|
||||
} else if workingDir != "" {
|
||||
return "", errors.New("Working Directory must be an absolute path")
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
return stdBuffer.String(), err
|
||||
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/collector/pkg/errors"
|
||||
"github.com/analogj/scrutiny/collector/pkg/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
type MetricsCollector struct {
|
||||
BaseCollector
|
||||
|
||||
apiEndpoint *url.URL
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func CreateMetricsCollector(logger *logrus.Entry, apiEndpoint string) (MetricsCollector, error) {
|
||||
apiEndpointUrl, err := url.Parse(apiEndpoint)
|
||||
if err != nil {
|
||||
return MetricsCollector{}, err
|
||||
}
|
||||
|
||||
sc := MetricsCollector{
|
||||
apiEndpoint: apiEndpointUrl,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func (mc *MetricsCollector) Run() error {
|
||||
err := mc.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
|
||||
apiEndpoint.Path = "/api/devices"
|
||||
|
||||
deviceRespWrapper := new(models.DeviceRespWrapper)
|
||||
|
||||
fmt.Println("Getting devices")
|
||||
err = mc.getJson(apiEndpoint.String(), &deviceRespWrapper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !deviceRespWrapper.Success {
|
||||
//TODO print error payload
|
||||
fmt.Println("An error occurred while retrieving devices")
|
||||
} else {
|
||||
fmt.Println(deviceRespWrapper)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, device := range deviceRespWrapper.Data {
|
||||
// execute collection in parallel go-routines
|
||||
wg.Add(1)
|
||||
go mc.Collect(&wg, device.WWN, device.DeviceName)
|
||||
}
|
||||
|
||||
fmt.Println("Main: Waiting for workers to finish")
|
||||
wg.Wait()
|
||||
fmt.Println("Main: Completed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *MetricsCollector) Validate() error {
|
||||
fmt.Println("Verifying required tools")
|
||||
_, lookErr := exec.LookPath("smartctl")
|
||||
|
||||
if lookErr != nil {
|
||||
return errors.DependencyMissingError("smartctl is missing")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *MetricsCollector) Collect(wg *sync.WaitGroup, deviceWWN string, deviceName string) {
|
||||
defer wg.Done()
|
||||
fmt.Printf("Collecting smartctl results for %s\n", deviceName)
|
||||
|
||||
result, err := mc.execCmd("smartctl", []string{"-a", "-j", fmt.Sprintf("/dev/%s", deviceName)}, "", nil)
|
||||
resultBytes := []byte(result)
|
||||
if err != nil {
|
||||
fmt.Printf("error while retrieving data from smartctl %s\n", deviceName)
|
||||
fmt.Printf("ERROR MESSAGE: %v", err)
|
||||
fmt.Printf("RESULT: %v", result)
|
||||
// TODO: error while retrieving data from smartctl.
|
||||
// TODO: we should pass this data on to scrutiny API for recording.
|
||||
return
|
||||
} else {
|
||||
//successful run, pass the results directly to webapp backend for parsing and processing.
|
||||
mc.Publish(deviceWWN, resultBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MetricsCollector) Publish(deviceWWN string, payload []byte) error {
|
||||
fmt.Printf("Publishing smartctl results for %s\n", deviceWWN)
|
||||
|
||||
apiEndpoint, _ := url.Parse(mc.apiEndpoint.String())
|
||||
apiEndpoint.Path = fmt.Sprintf("/api/device/%s/smart", strings.ToLower(deviceWWN))
|
||||
|
||||
resp, err := httpClient.Post(apiEndpoint.String(), "application/json", bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type SelfTestCollector struct {
|
||||
BaseCollector
|
||||
|
||||
apiEndpoint *url.URL
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func CreateSelfTestCollector(logger *logrus.Entry, apiEndpoint string) (SelfTestCollector, error) {
|
||||
apiEndpointUrl, err := url.Parse(apiEndpoint)
|
||||
if err != nil {
|
||||
return SelfTestCollector{}, err
|
||||
}
|
||||
|
||||
stc := SelfTestCollector{
|
||||
apiEndpoint: apiEndpointUrl,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
return stc, nil
|
||||
}
|
||||
|
||||
func (sc *SelfTestCollector) Run() error {
|
||||
return nil
|
||||
}
|
@ -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))
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package errors_test
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/collector/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")
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
type Device struct {
|
||||
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_name"`
|
||||
Capacity int64 `json:"capacity"`
|
||||
Firmware string `json:"firmware"`
|
||||
RotationSpeed int `json:"rotational_speed"`
|
||||
}
|
||||
|
||||
type DeviceRespWrapper struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []error `json:"errors"`
|
||||
Data []Device `json:"data"`
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package models
|
||||
|
||||
//type SelfTest struct {
|
||||
// DeviceWWN string
|
||||
// Device Device `gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
|
||||
//
|
||||
// TestDate time.Time
|
||||
//}
|
@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
//type Smart struct {
|
||||
// DeviceWWN string
|
||||
// Device Device `gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
|
||||
//
|
||||
// TestDate time.Time
|
||||
//
|
||||
// Temp float32
|
||||
// PowerOnCount int64
|
||||
// PowerOnHours int64
|
||||
// SmartStatus string
|
||||
// SmartAttributes string
|
||||
//}
|
@ -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"
|
After Width: | Height: | Size: 450 KiB |
After Width: | Height: | Size: 218 KiB |
After Width: | Height: | Size: 463 KiB |
@ -0,0 +1,54 @@
|
||||
# Commented Scrutiny Configuration File
|
||||
#
|
||||
# The default location for this file is ~/scrutiny.yaml.
|
||||
# In some cases to improve clarity default values are specified,
|
||||
# uncommented. Other example values are commented out.
|
||||
#
|
||||
# When this file is parsed by Scrutiny, all configuration file keys are
|
||||
# lowercased automatically. As such, Configuration keys are case-insensitive,
|
||||
# and should be lowercase in this file to be consistent with usage.
|
||||
|
||||
|
||||
######################################################################
|
||||
# Version
|
||||
#
|
||||
# version specifies the version of this configuration file schema, not
|
||||
# the scrutiny binary. There is only 1 version available at the moment
|
||||
version: 1
|
||||
|
||||
web:
|
||||
listen:
|
||||
port: 8080
|
||||
host: 0.0.0.0
|
||||
database:
|
||||
# can also set absolute path here
|
||||
location: ./scrutiny.db
|
||||
src:
|
||||
frontend:
|
||||
path: ./dist
|
||||
|
||||
disks:
|
||||
include:
|
||||
# - /dev/sda
|
||||
exclude:
|
||||
# - /dev/sdb
|
||||
|
||||
notify:
|
||||
metric:
|
||||
script: 'notify-metrics.sh'
|
||||
long:
|
||||
script: 'notify-long-test.sh'
|
||||
short:
|
||||
script: 'notify-short-test.sh'
|
||||
|
||||
|
||||
collect:
|
||||
metric:
|
||||
enable: true
|
||||
command: '-a -o on -S on'
|
||||
long:
|
||||
enable: false
|
||||
command: ''
|
||||
short:
|
||||
enable: false
|
||||
command: ''
|
@ -0,0 +1,19 @@
|
||||
module github.com/analogj/scrutiny
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/jaypipes/ghw v0.6.1
|
||||
github.com/jinzhu/gorm v1.9.14
|
||||
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.2.0
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
)
|
@ -0,0 +1,413 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14 h1:wsrSjiqQtseStRIoLLxS4C5IEtXkazZVEPDHq8jW7r8=
|
||||
github.com/analogj/go-util v0.0.0-20190301173314-5295e364eb14/go.mod h1:lJQVqFKMV5/oDGYR2bra2OljcF3CvolAoyDRyOA4k4E=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jaypipes/ghw v0.6.1 h1:Ewt3mdpiyhWotGyzg1ursV/6SnToGcG4215X6rR2af8=
|
||||
github.com/jaypipes/ghw v0.6.1/go.mod h1:QOXppNRCLGYR1H+hu09FxZPqjNt09bqUZUnOL3Rcero=
|
||||
github.com/jaypipes/pcidb v0.5.0 h1:4W5gZ+G7QxydevI8/MmmKdnIPJpURqJ2JNXTzfLxF5c=
|
||||
github.com/jaypipes/pcidb v0.5.0/go.mod h1:L2RGk04sfRhp5wvHO0gfRAMoLY/F3PKv/nwJeVoho0o=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jinzhu/gorm v1.9.14 h1:Kg3ShyTPcM6nzVo148fRrcMO6MNKuqtOUwnzqMgVniM=
|
||||
github.com/jinzhu/gorm v1.9.14/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 h1:3tLzEnUizyN9YLWFTT9loC30lSBvh2y70LTDcZOTs1s=
|
||||
github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
|
||||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/execlineb -S0
|
||||
|
||||
echo "jobber/cron exiting"
|
||||
s6-svscanctl -t /var/run/s6/services
|
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
echo "starting jobber/cron"
|
||||
|
||||
su -c "/usr/lib/x86_64-linux-gnu/jobberrunner /scrutiny/config/jobber.yaml" root
|
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
echo "starting scrutiny"
|
||||
|
||||
scrutiny start
|
@ -0,0 +1,30 @@
|
||||
version: 1.4
|
||||
|
||||
prefs:
|
||||
logPath: /scrutiny/jobber/log.log
|
||||
runLog:
|
||||
type: file
|
||||
path: /scrutiny/jobber/runlog
|
||||
maxFileLen: 100m
|
||||
maxHistories: 2
|
||||
|
||||
resultSinks:
|
||||
- &filesystemSink
|
||||
type: filesystem
|
||||
path: /scrutiny/jobber
|
||||
data:
|
||||
- stdout
|
||||
- stderr
|
||||
maxAgeDays: 10
|
||||
|
||||
jobs:
|
||||
MetricsJob:
|
||||
cmd: /scrutiny/bin/scrutiny-collector-metrics run --api-endpoint ${SCRUTINY_API_ENDPOINT:-http://localhost:8080}
|
||||
# run daily at midnight.
|
||||
time: '0 0 * * *'
|
||||
onError: Backoff
|
||||
notifyOnSuccess:
|
||||
- *filesystemSink
|
||||
notifyOnFailure:
|
||||
- *filesystemSink
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
@ -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"`
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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"
|
@ -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.
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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)
|
@ -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).
|
@ -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"
|
||||
}
|
@ -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'.
|
@ -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}}));
|
||||
}
|
||||
};
|
@ -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));
|
||||
});
|
||||
});
|
@ -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>;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
@ -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
|
||||
});
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
{
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue