├── .argo └── claudia-ci.yaml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile-builder ├── LICENSE.md ├── README.md ├── VERSION ├── ami ├── docker-compose-ami.yml.in └── docker-storage ├── billingbucket └── billingbucket.go ├── build.py ├── build.sh ├── claudiad └── main.go ├── config.env ├── constants.go ├── costdb ├── costdb.go ├── dimensions.go └── ingest.go ├── docker-compose-debug.yml ├── docker-compose-dev.yml ├── docker-compose.yml ├── docs ├── about │ ├── contact.md │ └── release-notes.md ├── faq.md ├── index.md ├── report-screenshot.png └── setup │ ├── aws.md │ ├── aws │ ├── attach-policy.png │ ├── copy-credentials.png │ ├── create-report-1.png │ ├── create-report-2.png │ ├── create-report-3.png │ ├── create-user.png │ ├── enable-createdby-tag.png │ └── enable-user-tags.png │ ├── claudia.md │ └── claudia │ ├── account-aliases.png │ ├── add-bucket.png │ ├── bucket-information.png │ ├── http-sg.png │ └── manage-certificates.png ├── errors └── errors.go ├── glide.lock ├── glide.yaml ├── gometalinter.json ├── ingest ├── processor.go └── service.go ├── ingestd └── main.go ├── mkdocs.yml ├── packer.json ├── parser ├── parser.go └── region.go ├── routers ├── auth.go ├── cost.go ├── reports.go └── routers.go ├── server └── server.go ├── ui ├── .babelrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .nvmrc ├── Dockerfile ├── README.md ├── build.sh ├── config │ ├── head-config.common.js │ ├── helpers.js │ ├── html-elements-plugin │ │ └── index.js │ ├── karma.conf.js │ ├── modules │ │ └── angular2-hmr-prod.js │ ├── protractor.conf.js │ ├── spec-bundle.js │ ├── webpack.common.js │ ├── webpack.dev.js │ └── webpack.prod.js ├── karma.conf.js ├── npm-shrinkwrap.json ├── package.json ├── protractor.conf.js ├── src │ ├── app │ │ ├── +user-management │ │ │ ├── change-password │ │ │ │ ├── change-password-data.ts │ │ │ │ ├── change-password.component.html │ │ │ │ └── change-password.component.ts │ │ │ ├── index.ts │ │ │ ├── user-details │ │ │ │ ├── user-details.component.html │ │ │ │ ├── user-details.component.scss │ │ │ │ └── user-details.component.ts │ │ │ └── user-management.module.ts │ │ ├── accounts │ │ │ ├── accounts.component.html │ │ │ ├── accounts.component.scss │ │ │ ├── accounts.component.ts │ │ │ └── index.ts │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.error-handler.ts │ │ ├── app.module.ts │ │ ├── app.routes.ts │ │ ├── app.service.ts │ │ ├── common │ │ │ ├── abstract-value-accessor │ │ │ │ └── abstract-value-accessor.ts │ │ │ ├── ax-lib │ │ │ │ ├── ax-lib.module.ts │ │ │ │ ├── checkbox-slide │ │ │ │ │ ├── checkbox-slide.component.html │ │ │ │ │ ├── checkbox-slide.component.scss │ │ │ │ │ ├── checkbox-slide.component.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── checkbox │ │ │ │ │ ├── checkbox.component.html │ │ │ │ │ ├── checkbox.component.scss │ │ │ │ │ ├── checkbox.component.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── date-range │ │ │ │ │ ├── _date-range.scss │ │ │ │ │ ├── date-range.component.ts │ │ │ │ │ ├── date-range.html │ │ │ │ │ ├── date-range.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── dropdown-multiselect │ │ │ │ │ ├── dropdown-multiselect.component.ts │ │ │ │ │ ├── dropdown-multiselect.html │ │ │ │ │ └── dropdown-multiselect.scss │ │ │ │ ├── dropdown │ │ │ │ │ ├── _dropdown.scss │ │ │ │ │ └── dropdown.component.ts │ │ │ │ ├── help-panel │ │ │ │ │ ├── help-panel.component.html │ │ │ │ │ ├── help-panel.component.scss │ │ │ │ │ ├── help-panel.component.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── radio-button │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── radio-button.component.html │ │ │ │ │ ├── radio-button.component.scss │ │ │ │ │ └── radio-button.component.ts │ │ │ │ ├── services │ │ │ │ │ ├── http.service.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── sliding-panel │ │ │ │ │ ├── _sliding-panel.component.scss │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── sliding-panel.component.html │ │ │ │ │ └── sliding-panel.component.ts │ │ │ │ ├── sort-arrow │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── sort-arrow.component.html │ │ │ │ │ ├── sort-arrow.component.scss │ │ │ │ │ └── sort-arrow.component.ts │ │ │ │ └── switch │ │ │ │ │ ├── switch.component.ts │ │ │ │ │ ├── switch.html │ │ │ │ │ └── switch.scss │ │ │ ├── collapse-box │ │ │ │ ├── collapse-box.component.html │ │ │ │ ├── collapse-box.component.scss │ │ │ │ ├── collapse-box.component.ts │ │ │ │ ├── collapse-box.module.ts │ │ │ │ └── index.ts │ │ │ ├── http-interceptor.ts │ │ │ ├── index.ts │ │ │ ├── msg-box │ │ │ │ ├── index.ts │ │ │ │ ├── msg-box.component.html │ │ │ │ ├── msg-box.component.scss │ │ │ │ ├── msg-box.component.ts │ │ │ │ └── msg-box.module.ts │ │ │ ├── notifications │ │ │ │ ├── index.ts │ │ │ │ ├── notification.model.ts │ │ │ │ ├── notifications.component.html │ │ │ │ ├── notifications.component.scss │ │ │ │ ├── notifications.component.ts │ │ │ │ ├── notifications.module.ts │ │ │ │ └── notifications.service.ts │ │ │ ├── password-macher.ts │ │ │ ├── shared │ │ │ │ ├── constants.ts │ │ │ │ └── index.ts │ │ │ └── validate-onblur.ts │ │ ├── config │ │ │ ├── aws-config.component.html │ │ │ ├── aws-config.component.scss │ │ │ ├── aws-config.component.ts │ │ │ ├── bucket.component.html │ │ │ ├── bucket.component.scss │ │ │ ├── bucket.component.ts │ │ │ ├── config.component.html │ │ │ ├── config.component.scss │ │ │ ├── config.component.ts │ │ │ ├── index.ts │ │ │ └── keys-config │ │ │ │ ├── keys-config.component.html │ │ │ │ ├── keys-config.component.scss │ │ │ │ └── keys-config.component.ts │ │ ├── dashboard │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.ts │ │ │ └── index.ts │ │ ├── environment.ts │ │ ├── error-page │ │ │ ├── error-page.component.ts │ │ │ ├── error-page.html │ │ │ └── index.ts │ │ ├── eula │ │ │ ├── eula.component.html │ │ │ ├── eula.component.ts │ │ │ ├── eula.scss │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── layout │ │ │ ├── base │ │ │ │ ├── base-layout.component.html │ │ │ │ ├── base-layout.component.scss │ │ │ │ ├── base-layout.component.ts │ │ │ │ ├── base-layout.module.ts │ │ │ │ ├── index.ts │ │ │ │ └── topbar │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── topbar.component.html │ │ │ │ │ ├── topbar.component.scss │ │ │ │ │ └── topbar.component.ts │ │ │ ├── index.ts │ │ │ └── intro │ │ │ │ ├── index.ts │ │ │ │ ├── intro-layout.component.html │ │ │ │ ├── intro-layout.component.scss │ │ │ │ ├── intro-layout.component.ts │ │ │ │ └── intro-layout.module.ts │ │ ├── login │ │ │ ├── index.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ └── login.component.ts │ │ ├── model │ │ │ ├── account.model.ts │ │ │ ├── bucket.model.ts │ │ │ ├── config.model.ts │ │ │ ├── dimension.model.ts │ │ │ ├── graph.model.ts │ │ │ ├── index.ts │ │ │ ├── report.model.ts │ │ │ ├── usage.model.ts │ │ │ └── user.model.ts │ │ ├── no-content │ │ │ ├── index.ts │ │ │ └── no-content.component.ts │ │ ├── password │ │ │ ├── index.ts │ │ │ ├── password-data.ts │ │ │ ├── password.component.html │ │ │ └── password.component.ts │ │ ├── pipe │ │ │ ├── axc-pipe.module.ts │ │ │ ├── date.pipe.ts │ │ │ ├── index.ts │ │ │ └── trim-text.pipe.ts │ │ ├── report │ │ │ ├── advanced-filter.component.html │ │ │ ├── advanced-filter.component.ts │ │ │ ├── advanced-filter.scss │ │ │ ├── config.ts │ │ │ ├── data-chart │ │ │ │ ├── data-chart.component.html │ │ │ │ ├── data-chart.component.ts │ │ │ │ ├── data-chart.scss │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── report.component.html │ │ │ ├── report.component.ts │ │ │ ├── reporting.scss │ │ │ └── tags-filter │ │ │ │ ├── tags-filter.component.html │ │ │ │ └── tags-filter.component.ts │ │ ├── resolvers │ │ │ ├── has-no-session.resolver.ts │ │ │ ├── report.resolver.ts │ │ │ └── who-am-i.resolver.ts │ │ ├── services │ │ │ ├── authentication.service.ts │ │ │ ├── config.service.ts │ │ │ ├── index.ts │ │ │ ├── reporting.service.ts │ │ │ └── user.service.ts │ │ └── settings │ │ │ ├── index.ts │ │ │ ├── settings.component.html │ │ │ ├── settings.component.scss │ │ │ └── settings.component.ts │ ├── assets │ │ ├── css │ │ │ ├── _base.scss │ │ │ ├── _config.scss │ │ │ ├── _reset.scss │ │ │ ├── _typography.scss │ │ │ ├── elements │ │ │ │ ├── _buttons.scss │ │ │ │ ├── _dropdown.scss │ │ │ │ ├── _form.scss │ │ │ │ ├── _list-group.scss │ │ │ │ ├── _table.scss │ │ │ │ ├── _tag.scss │ │ │ │ ├── _toolbar.scss │ │ │ │ └── _white-box.scss │ │ │ └── main.scss │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-256x256.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── manifest.json │ │ │ ├── mstile-150x150.png │ │ │ └── safari-pinned-tab.svg │ │ └── icons │ │ │ ├── AXClaudia_logo.png │ │ │ ├── AX_Logo.svg │ │ │ ├── axlogo_teal.svg │ │ │ ├── full_logo.svg │ │ │ ├── ico_environments.svg │ │ │ ├── ico_failed.svg │ │ │ ├── ico_success.svg │ │ │ ├── ico_users.svg │ │ │ └── logo.png │ ├── custom-typings.d.ts │ ├── index.html │ ├── main.browser.ts │ ├── polyfills.browser.ts │ └── vendor.browser.ts ├── tsconfig.json ├── tslint.json ├── typedoc.json └── webpack.config.js ├── userdb ├── schema.go ├── session.go └── userdb.go ├── util └── util.go └── version.go /.argo/claudia-ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | type: workflow 3 | version: 1 4 | name: Claudia Build 5 | description: Claudia Build Workflow 6 | inputs: 7 | parameters: 8 | COMMIT: 9 | default: "%%session.commit%%" 10 | REPO: 11 | default: "%%session.repo%%" 12 | steps: 13 | - CHECKOUT: 14 | image: argoproj/argoscm:v2.0 15 | command: ["axscm"] 16 | args: ["clone", "%%inputs.parameters.REPO%%", "/src", "--commit", "%%inputs.parameters.COMMIT%%"] 17 | resources: 18 | mem_mib: 256 19 | cpu_cores: 0.1 20 | outputs: 21 | artifacts: 22 | CODE: 23 | path: /src 24 | - BUILD: 25 | image: applatix/claudia-builder:latest 26 | command: [/src/build.py] 27 | args: [""] # Workaround argo issue https://github.com/argoproj/argo/issues/97 28 | inputs: 29 | artifacts: 30 | CODE: 31 | from: "%%steps.CHECKOUT.outputs.artifacts.CODE%%" 32 | path: "/src" 33 | resources: 34 | mem_mib: 256 35 | cpu_cores: 0.1 36 | annotations: 37 | ax_ea_docker_enable: '{ "graph-storage-name": "claudia-build", "graph-storage-size": "10Gi", "mem_mib": 1024, "cpu_cores": 0.3 }' 38 | 39 | --- 40 | type: policy 41 | version: 1 42 | name: Claudia Build Policy 43 | description: Policy to trigger build for all events 44 | template: Claudia Build 45 | notifications: 46 | - when: 47 | - on_success 48 | - on_failure 49 | whom: 50 | - committer 51 | - author 52 | when: 53 | - event: on_push 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .vscode/ 3 | .DS_Store 4 | ami/claudia.tar.gz 5 | ami/docker-compose.yml 6 | # Delve debug binaries 7 | */debug 8 | .idea/ 9 | site/ 10 | dist/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### Version 1.1.0 (2017-08-30) 4 | * Open source release 5 | * Cache bucket region during configuration 6 | 7 | ### Version 1.0.9 (2017-04-27) 8 | * Fix issue preventing the ingestion of reports with Redshift/QuickSight option enabled 9 | * Better error reporting to UI 10 | 11 | ### Version 1.0.8 (2017-04-25) 12 | * Fix issue where some metrics (e.g. instance hours) would appear twice in the metrics selector 13 | 14 | ### Version 1.0.7 (2017-04-20) 15 | * Eliminate the GetBucketLocation requirement for the Claudia IAM policy 16 | 17 | ### Version 1.0.6 (2017-04-11) 18 | * Accomodate changes to AWS Cost & Usage reports with respect to reserved instance pricing 19 | 20 | ### Version 1.0.5 (2017-04-08) 21 | * Improved region detection of billing report line items 22 | 23 | ### Version 1.0.4 (2017-04-07) 24 | * Add support for AWS GovCloud (US) region 25 | * Better handling of new regions 26 | 27 | ### Version 1.0.3 (2017-04-04) 28 | * Fix issue preventing the configuration of a bucket residing in us-east-1 29 | 30 | ### Version 1.0.2 (2017-03-17) 31 | * Add indicator when billing reports are currently being processed 32 | * When selecting hourly interval, the hour of the datapoint is properly displayed in the tooltip 33 | * Support report names and paths with special characters, and reports which did not configure a "report path prefix" 34 | * Eliminate some resource leaks 35 | 36 | ### Version 1.0.1 (2017-02-23) 37 | * Initial release -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for the Claudia API/web server, and ingest daemon 2 | 3 | FROM debian:9.1 4 | 5 | RUN apt-get update && apt-get install -y \ 6 | # ca-certificates required for AWS SDK 7 | ca-certificates && \ 8 | apt-get clean && \ 9 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 10 | 11 | EXPOSE 80 443 12 | 13 | COPY dist/bin/* /usr/bin/ 14 | COPY ui/dist /var/lib/claudia/assets 15 | 16 | CMD ["/usr/bin/claudiad"] 17 | -------------------------------------------------------------------------------- /Dockerfile-builder: -------------------------------------------------------------------------------- 1 | # This container creates the claudia builder image, which contains the environment necessary to build this project (e.g. golang, node) 2 | 3 | FROM debian:9.1 4 | 5 | # Install python, git, packer, mkdocs 6 | # NOTE: python2 is installed over python3 because python2 eventually gets installed 7 | # by the nodejs setup script anyways, and mkdocs prefers python2 over python3 8 | # because of click incompatibilites. 9 | RUN apt-get update && apt-get install -y \ 10 | wget \ 11 | python-minimal \ 12 | git \ 13 | curl \ 14 | gcc \ 15 | gnupg \ 16 | unzip && \ 17 | curl https://bootstrap.pypa.io/get-pip.py | python && \ 18 | pip install --no-cache-dir mkdocs==0.16.1 && \ 19 | wget https://releases.hashicorp.com/packer/0.12.2/packer_0.12.2_linux_amd64.zip && \ 20 | unzip -d /usr/bin packer*.zip && \ 21 | rm -f packer*.zip && \ 22 | apt-get clean && \ 23 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 24 | 25 | # Install docker (necessary for using this image as the build container during Argo CI) 26 | ENV DOCKER_BUCKET get.docker.com 27 | ENV DOCKER_VERSION 1.11.2 28 | ENV DOCKER_SHA256 8c2e0c35e3cda11706f54b2d46c2521a6e9026a7b13c7d4b8ae1f3a706fc55e1 29 | 30 | RUN set -x \ 31 | && curl -fSL "https://${DOCKER_BUCKET}/builds/Linux/x86_64/docker-${DOCKER_VERSION}.tgz" -o docker.tgz \ 32 | && echo "${DOCKER_SHA256} *docker.tgz" | sha256sum -c - \ 33 | && tar -xzvf docker.tgz \ 34 | && mv docker/* /usr/local/bin/ \ 35 | && rmdir docker \ 36 | && rm docker.tgz \ 37 | && docker -v 38 | 39 | # Install go 40 | ENV GO_VERSION 1.8.3 41 | ENV GO_ARCH amd64 42 | RUN wget https://storage.googleapis.com/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \ 43 | tar -C /usr/local/ -xf /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \ 44 | rm /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz 45 | 46 | # Install nodejs 47 | RUN curl -sL https://deb.nodesource.com/setup_7.x | bash - && apt-get install -y nodejs && \ 48 | apt-get clean && \ 49 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 50 | 51 | ENV GOPATH /root/go 52 | ENV PATH ${GOPATH}/bin:/usr/local/go/bin:${PATH} 53 | 54 | # Install glide 55 | RUN mkdir -p ${GOPATH}/bin && \ 56 | mkdir -p ${GOPATH}/pkg && \ 57 | mkdir -p ${GOPATH}/src && \ 58 | wget https://glide.sh/get && \ 59 | chmod ugo+x ./get && \ 60 | ./get && \ 61 | rm -f get && \ 62 | rm -rf /tmp/glide* 63 | 64 | # Install Go dependencies and some tooling 65 | COPY glide.yaml ${GOPATH} 66 | COPY glide.lock ${GOPATH} 67 | RUN cd ${GOPATH} && \ 68 | glide install && \ 69 | mv vendor/* src/ && \ 70 | rmdir vendor && \ 71 | go get -u -v github.com/derekparker/delve/cmd/dlv && \ 72 | go get -u -v gopkg.in/alecthomas/gometalinter.v1 && \ 73 | cd ${GOPATH}/bin && \ 74 | ln -s gometalinter.v1 gometalinter && \ 75 | gometalinter --install 76 | 77 | # Install Node dependencies 78 | COPY ui/package.json /root/node/package.json 79 | COPY ui/npm-shrinkwrap.json /root/node/npm-shrinkwrap.json 80 | RUN cd /root/node && npm install 81 | 82 | # MkDocs Themes 83 | RUN pip install mkdocs-bootswatch 84 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Applatix, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License.. 14 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.1 -------------------------------------------------------------------------------- /ami/docker-compose-ami.yml.in: -------------------------------------------------------------------------------- 1 | # AMI version of app which exposes only the http port and expects EBS volume mounted under /mnt/claudia 2 | version: '2' 3 | services: 4 | costdb: 5 | image: influxdb:1.2.0 6 | expose: 7 | - "8086" 8 | environment: 9 | - INFLUXDB_HTTP_MAX_ROW_LIMIT=40000 10 | - INFLUXDB_REPORTING_DISABLED=true 11 | volumes: 12 | - /mnt/claudia/influxdb:/var/lib/influxdb 13 | restart: always 14 | userdb: 15 | image: postgres:9.6.1 16 | expose: 17 | - "5432" 18 | volumes: 19 | - /mnt/claudia/postgres:/var/lib/postgresql/data 20 | env_file: 21 | - ./config.env 22 | restart: always 23 | ingestd: 24 | image: "${IMAGE_TAG}" 25 | entrypoint: 26 | - /usr/bin/ingestd 27 | - run 28 | depends_on: 29 | - costdb 30 | - userdb 31 | expose: 32 | - "8081" 33 | env_file: 34 | - ./config.env 35 | restart: always 36 | server: 37 | image: "${IMAGE_TAG}" 38 | depends_on: 39 | - costdb 40 | - userdb 41 | ports: 42 | - "80:80" 43 | - "443:443" 44 | env_file: 45 | - ./config.env 46 | restart: always 47 | -------------------------------------------------------------------------------- /ami/docker-storage: -------------------------------------------------------------------------------- 1 | DOCKER_STORAGE_OPTIONS="--storage-driver=overlay2" -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Build the binary in native platform. This script is invoked inside the builder container 3 | # as well as for development purposes (e.g. Mac) 4 | 5 | set -xe 6 | SRCROOT=$(cd $(dirname $0);pwd) 7 | 8 | PACKAGE_NAME="github.com/applatix/claudia" 9 | 10 | echo "Linting source" 11 | gometalinter --config ${SRCROOT}/gometalinter.json --deadline 2m ${SRCROOT}/... 12 | 13 | CLAUDIA_VERSION=$(cat $SRCROOT/VERSION) 14 | CLAUDIA_REVISION=$(git -C $SRCROOT rev-parse --short=7 HEAD) 15 | dirty=$(git -C $SRCROOT status --porcelain) 16 | if [ ! -z "$dirty" ]; then 17 | CLAUDIA_REVISION="${CLAUDIA_REVISION}-dirty" 18 | fi 19 | BUILD_DATE=$(date -u '+%Y-%m-%dT%H:%M:%S') 20 | 21 | # GOBIN is manupulated so that binaries end up in the project directory, 22 | # so that we can subsequently package it into a container. 23 | export GOBIN="$SRCROOT/dist/bin" 24 | # -pkgdir is used to allow repeat builds to complete 25 | PKGDIR="$SRCROOT/dist/$(go env GOOS)_$(go env GOARCH)" 26 | 27 | LD_FLAGS="-X $PACKAGE_NAME.Version=$CLAUDIA_VERSION -X $PACKAGE_NAME.Revision=$CLAUDIA_REVISION -X $PACKAGE_NAME.BuildDate=$BUILD_DATE" 28 | # purge any precompiled claudia archive files in order to guarantee version 29 | # and build date information is updated in the resulting binaries. 30 | # `go install` will decide to skip the build if it thinks there were no 31 | # source code changes, despite passing different LD_FLAGS. 32 | rm -rf "$PKGDIR/$PACKAGE_NAME" 33 | go env 34 | go install -v -pkgdir "$PKGDIR" -ldflags "$LD_FLAGS" $PACKAGE_NAME/claudiad 35 | go install -v -pkgdir "$PKGDIR" -ldflags "$LD_FLAGS" $PACKAGE_NAME/ingestd 36 | -------------------------------------------------------------------------------- /config.env: -------------------------------------------------------------------------------- 1 | POSTGRES_PASSWORD=my-secret-pw 2 | POSTGRES_DB=userdb 3 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Applatix, Inc. 2 | package claudia 3 | 4 | import "time" 5 | 6 | // Application constants 7 | const ( 8 | AWSMarketplaceAccountID = "679593333241" 9 | ) 10 | 11 | // Specially treated services 12 | const ( 13 | ServiceAWSS3 = "AWS S3" 14 | ServiceAWSEC2Instance = "AWS EC2 Instance" 15 | ServiceAWSMarketplace = "AWS Marketplace" 16 | ServiceAWSEBSVolume = "AWS EBS Volume" 17 | ServiceAWSCloudWatch = "AWS CloudWatch" 18 | ServiceAWSCloudFront = "AWS CloudFront" 19 | ServiceAWSEC2DataTransfer = "AWS EC2 Data Transfer" 20 | ) 21 | 22 | // Application configuration settings 23 | var ( 24 | ApplicationPort = 443 25 | ApplicationDir = "/var/lib/claudia" 26 | ApplicationAdminUsername = "admin" 27 | ApplicationAdminDefaultPassword = "password" 28 | IngestdURL = "http://ingestd:8081" 29 | IngestdPort = 8081 30 | IngestdWorkers = 2 31 | IngestdBatchInterval = 5000 32 | IngestStatusMeasurementName = "ingest_status" 33 | IngestdWriteRetryDelay = []time.Duration{10 * time.Second, 20 * time.Second, 40 * time.Second, 60 * time.Second, 120 * time.Second} 34 | CostDatabaseURL = "http://costdb:8086" 35 | CostDatabaseName = "cost_usage" 36 | ReportDefaultRetentionDays = 365 37 | ) 38 | 39 | // ReportStatus is the status of a report. One of: "processing", "error", "current" 40 | type ReportStatus string 41 | 42 | // Valid report statuses 43 | const ( 44 | ReportStatusCurrent ReportStatus = "current" 45 | ReportStatusError ReportStatus = "error" 46 | ReportStatusProcessing ReportStatus = "processing" 47 | ) 48 | -------------------------------------------------------------------------------- /docker-compose-debug.yml: -------------------------------------------------------------------------------- 1 | # "Debug" version of app which exposes database ports and also maps source directory to container and leaves container running 2 | version: '2' 3 | services: 4 | costdb: 5 | image: influxdb:1.2.0 6 | expose: 7 | - "8086" 8 | - "8083" 9 | ports: 10 | - "8086:8086" 11 | - "8083:8083" 12 | environment: 13 | - INFLUXDB_HTTP_MAX_ROW_LIMIT=40000 14 | - INFLUXDB_ADMIN_ENABLED=true 15 | volumes: 16 | - costdb_vol:/var/lib/influxdb 17 | userdb: 18 | image: postgres:9.6.1 19 | expose: 20 | - "5432" 21 | ports: 22 | - "5432:5432" 23 | volumes: 24 | - userdb_vol:/var/lib/postgresql/data 25 | env_file: 26 | - ./config.env 27 | ingestd: 28 | image: claudia 29 | entrypoint: 30 | - "sh" 31 | - "-c" 32 | - "/usr/bin/ingestd run & sleep 365d" 33 | depends_on: 34 | - costdb 35 | - userdb 36 | expose: 37 | - "8081" 38 | ports: 39 | - "8081:8081" 40 | env_file: 41 | - ./config.env 42 | server: 43 | image: claudia 44 | depends_on: 45 | - costdb 46 | - userdb 47 | expose: 48 | - "80" 49 | - "443" 50 | ports: 51 | - "80:80" 52 | - "443:443" 53 | entrypoint: 54 | - "sh" 55 | - "-c" 56 | - "/usr/bin/claudiad & sleep 365d" 57 | env_file: 58 | - ./config.env 59 | volumes: 60 | - .:/src 61 | volumes: 62 | costdb_vol: 63 | userdb_vol: 64 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | # Development version of compose file which starts only the database services and exposes their ports 2 | # to the host. This way api servers can be developed against them outside the context of linked containers. 3 | version: '2' 4 | services: 5 | costdb: 6 | image: influxdb:1.2.0 7 | expose: 8 | - "8086" 9 | - "8083" 10 | ports: 11 | - "8086:8086" 12 | - "8083:8083" 13 | environment: 14 | - INFLUXDB_HTTP_MAX_ROW_LIMIT=40000 15 | - INFLUXDB_ADMIN_ENABLED=true 16 | volumes: 17 | - costdb_vol:/var/lib/influxdb 18 | userdb: 19 | image: postgres:9.6.1 20 | expose: 21 | - "5432" 22 | ports: 23 | - "5432:5432" 24 | volumes: 25 | - userdb_vol:/var/lib/postgresql/data 26 | env_file: 27 | - ./config.env 28 | volumes: 29 | costdb_vol: 30 | userdb_vol: 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # "Production" version of app which exposes only the http port 2 | version: '2' 3 | services: 4 | costdb: 5 | image: influxdb:1.2.0 6 | expose: 7 | - "8086" 8 | environment: 9 | - INFLUXDB_HTTP_MAX_ROW_LIMIT=40000 10 | - INFLUXDB_REPORTING_DISABLED=true 11 | volumes: 12 | - costdb_vol:/var/lib/influxdb 13 | restart: always 14 | userdb: 15 | image: postgres:9.6.1 16 | expose: 17 | - "5432" 18 | volumes: 19 | - userdb_vol:/var/lib/postgresql/data 20 | env_file: 21 | - ./config.env 22 | restart: always 23 | ingestd: 24 | image: claudia 25 | entrypoint: 26 | - /usr/bin/ingestd 27 | - run 28 | depends_on: 29 | - costdb 30 | - userdb 31 | expose: 32 | - "8081" 33 | env_file: 34 | - ./config.env 35 | restart: always 36 | server: 37 | image: claudia 38 | depends_on: 39 | - costdb 40 | - userdb 41 | ports: 42 | - "80:80" 43 | - "443:443" 44 | env_file: 45 | - ./config.env 46 | restart: always 47 | volumes: 48 | costdb_vol: 49 | userdb_vol: 50 | -------------------------------------------------------------------------------- /docs/about/contact.md: -------------------------------------------------------------------------------- 1 | Redirecting... 2 | -------------------------------------------------------------------------------- /docs/about/release-notes.md: -------------------------------------------------------------------------------- 1 | ../../CHANGELOG.md -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | #### How frequently is the data updated? 4 | AWS generates and delivers a new Cost and Usage report at least once a day to your billing bucket. Claudia checks once an hour for any new reports. 5 | 6 | #### What instance type should I use to run the app? 7 | Choosing the appropriate instance type depends on several factors, which include 8 | 9 | * volume of data 10 | * retention length of the report 11 | * cardinality of the data (which is the uniqueness of values in a column) 12 | * performance expectations of queries 13 | 14 | In general, you want enough memory to ensure your application performs well. Applatix recommends a minimum of 8 GB of memory. So instance types such as m4.large, m3.large, and t2.large are good starting points. 15 | 16 | #### Why is there partially missing data on the last day of the month? 17 | That's because the data for the last day of the month has not been "finalized" by AWS. The process of finalizing the report can take up to several days. Until AWS delivers this report to the billing bucket, there can be partially missing data for the last day of each month. 18 | 19 | #### What is the difference between AWS Cost and Usage Reports vs. Detailed Billing Reports? 20 | AWS Cost and Usage Reports is a newer billing report format (introduced December 2015) versus the classic Detailed Billing Reports (DBR) format (introduced December 2012). 21 | 22 | Compared to the DBR, the Cost and Usage Reports provide additional columns, which enable more dimensions to explore the data. 23 | 24 | Most importantly, the Cost and Usage Reports now provide clear indications 25 | on what instance pricing (reserved versus "on demand") was applied to an instance during usage. This enables you to better understand the effectiveness of your reserved instance purchases. 26 | 27 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Claudia 2 | 3 | ## Introduction 4 | 5 | Claudia is a Cost Analytics solution that provides insights into your AWS cloud spending. 6 | 7 | ![Screenshot](report-screenshot.png) 8 | 9 | ## Key Features 10 | * Hourly to Monthly reporting of cost and usage 11 | * Deep drill down of spending by accounts, services, regions, and more 12 | * AWS Service level dimensions and metrics 13 | * Multiple cloud accounts organized into a single report 14 | * Tagging for categorizing your resources 15 | 16 | Try out Claudia by following our easy [install instructions](setup/claudia.md). 17 | 18 | ## Applatix 19 | In addition to using Claudia, you can also try our full cloud management solution including cost and resource optimization provided by Applatix. To request a demo, contact us at [https://applatix.com/contact/](https://applatix.com/contact/). -------------------------------------------------------------------------------- /docs/report-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/report-screenshot.png -------------------------------------------------------------------------------- /docs/setup/aws/attach-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/aws/attach-policy.png -------------------------------------------------------------------------------- /docs/setup/aws/copy-credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/aws/copy-credentials.png -------------------------------------------------------------------------------- /docs/setup/aws/create-report-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/aws/create-report-1.png -------------------------------------------------------------------------------- /docs/setup/aws/create-report-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/aws/create-report-2.png -------------------------------------------------------------------------------- /docs/setup/aws/create-report-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/aws/create-report-3.png -------------------------------------------------------------------------------- /docs/setup/aws/create-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/aws/create-user.png -------------------------------------------------------------------------------- /docs/setup/aws/enable-createdby-tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/aws/enable-createdby-tag.png -------------------------------------------------------------------------------- /docs/setup/aws/enable-user-tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/aws/enable-user-tags.png -------------------------------------------------------------------------------- /docs/setup/claudia/account-aliases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/claudia/account-aliases.png -------------------------------------------------------------------------------- /docs/setup/claudia/add-bucket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/claudia/add-bucket.png -------------------------------------------------------------------------------- /docs/setup/claudia/bucket-information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/claudia/bucket-information.png -------------------------------------------------------------------------------- /docs/setup/claudia/http-sg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/claudia/http-sg.png -------------------------------------------------------------------------------- /docs/setup/claudia/manage-certificates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/docs/setup/claudia/manage-certificates.png -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 819f63d9b5527227d331f8d0e2af3a8323efa623c567772fd5b5a030db3e17ee 2 | updated: 2017-02-02T00:22:52.030554787-08:00 3 | imports: 4 | - name: github.com/aws/aws-sdk-go 5 | version: 3f8f870ec9939e32b3372abf74d24e468bcd285d 6 | subpackages: 7 | - aws 8 | - aws/awserr 9 | - aws/awsutil 10 | - aws/client 11 | - aws/client/metadata 12 | - aws/corehandlers 13 | - aws/credentials 14 | - aws/credentials/ec2rolecreds 15 | - aws/credentials/endpointcreds 16 | - aws/credentials/stscreds 17 | - aws/defaults 18 | - aws/ec2metadata 19 | - aws/endpoints 20 | - aws/request 21 | - aws/session 22 | - aws/signer/v4 23 | - private/protocol 24 | - private/protocol/ec2query 25 | - private/protocol/query 26 | - private/protocol/query/queryutil 27 | - private/protocol/rest 28 | - private/protocol/restxml 29 | - private/protocol/xml/xmlutil 30 | - private/waiter 31 | - service/ec2 32 | - service/s3 33 | - service/s3/s3iface 34 | - service/s3/s3manager 35 | - service/sts 36 | - name: github.com/go-ini/ini 37 | version: 2ba15ac2dc9cdf88c110ec2dc0ced7fa45f5678c 38 | - name: github.com/gorilla/context 39 | version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 40 | - name: github.com/gorilla/handlers 41 | version: ee54c7b44cab12289237fb8631314790076e728b 42 | - name: github.com/gorilla/mux 43 | version: 0eeaf8392f5b04950925b8a69fe70f110fa7cbfc 44 | - name: github.com/gorilla/securecookie 45 | version: 667fe4e3466a040b780561fe9b51a83a3753eefc 46 | - name: github.com/gorilla/sessions 47 | version: ca9ada44574153444b00d3fd9c8559e4cc95f896 48 | - name: github.com/influxdata/influxdb 49 | version: b7bb7e8359642b6e071735b50ae41f5eb343fd42 50 | subpackages: 51 | - client/v2 52 | - models 53 | - pkg/escape 54 | - toml 55 | - name: github.com/jmespath/go-jmespath 56 | version: bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d 57 | - name: github.com/jmoiron/sqlx 58 | version: cac998c4f0959c19c638c523e374fa8e4e0bcfe3 59 | subpackages: 60 | - reflectx 61 | - name: github.com/lib/pq 62 | version: 5bf161122cd640c2a5a2c1d7fa49ea9befff31dd 63 | subpackages: 64 | - oid 65 | - name: github.com/pkg/errors 66 | version: 645ef00459ed84a119197bfb8d8205042c6df63d 67 | - name: github.com/urfave/cli 68 | version: 0bdeddeeb0f650497d603c4ad7b20cfe685682f6 69 | - name: golang.org/x/crypto 70 | version: 9477e0b78b9ac3d0b03822fd95422e2fe07627cd 71 | subpackages: 72 | - bcrypt 73 | - blowfish 74 | testImports: [] 75 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: . 2 | import: 3 | - package: github.com/aws/aws-sdk-go 4 | version: ~1.6.9 5 | subpackages: 6 | - aws 7 | - aws/credentials 8 | - aws/session 9 | - service/s3 10 | - service/s3/s3manager 11 | - package: github.com/gorilla/handlers 12 | version: ~1.1.0 13 | - package: github.com/gorilla/mux 14 | version: ~1.1.0 15 | - package: github.com/gorilla/securecookie 16 | version: ~1.1.0 17 | - package: github.com/gorilla/sessions 18 | version: ~1.1.0 19 | - package: github.com/influxdata/influxdb 20 | version: ~1.2.0 21 | subpackages: 22 | - client/v2 23 | - models 24 | - package: github.com/jmoiron/sqlx 25 | - package: github.com/lib/pq 26 | - package: github.com/urfave/cli 27 | version: ~1.19.1 28 | - package: golang.org/x/crypto 29 | subpackages: 30 | - bcrypt 31 | - package: github.com/pkg/errors 32 | version: ~0.8.0 33 | -------------------------------------------------------------------------------- /gometalinter.json: -------------------------------------------------------------------------------- 1 | { 2 | "DisableAll": true, 3 | "Enable": [ 4 | "vet", 5 | "deadcode", 6 | "errcheck", 7 | "varcheck", 8 | "structcheck", 9 | "ineffassign", 10 | "aligncheck", 11 | "unconvert" 12 | ] 13 | } -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Claudia Docs 2 | site_url: https://applatix.github.io/claudia/ 3 | theme: yeti 4 | copyright: Copyright © 2017 Applatix 5 | repo_url: https://github.com/applatix/claudia/ 6 | strict: true 7 | pages: 8 | - Home: 'index.md' 9 | - Setup: 10 | - AWS: 'setup/aws.md' 11 | - Claudia: 'setup/claudia.md' 12 | - FAQ: 'faq.md' 13 | - About: 14 | - 'Release Notes': 'about/release-notes.md' 15 | - 'Contact': 'about/contact.md' 16 | -------------------------------------------------------------------------------- /packer.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "aws_access_key": "", 4 | "aws_secret_key": "", 5 | "build_timestamp": "", 6 | "full_version": "", 7 | "version": "" 8 | }, 9 | "builders": [{ 10 | "type": "amazon-ebs", 11 | "access_key": "{{user `aws_access_key`}}", 12 | "secret_key": "{{user `aws_secret_key`}}", 13 | "region": "us-east-1", 14 | "source_ami": "ami-0b33d91d", 15 | "instance_type": "m3.medium", 16 | "ssh_username": "ec2-user", 17 | "ami_name": "claudia-v{{user `full_version`}}-{{user `build_timestamp`}}", 18 | "ami_description": "Claudia v{{user `version`}}", 19 | 20 | "ami_block_device_mappings": [ 21 | { 22 | "device_name": "/dev/xvdl", 23 | "volume_size": 40, 24 | "volume_type": "gp2", 25 | "delete_on_termination": true 26 | }], 27 | "launch_block_device_mappings": [ 28 | { 29 | "device_name": "/dev/xvdl", 30 | "volume_size": 40, 31 | "volume_type": "gp2", 32 | "delete_on_termination": true 33 | }] 34 | }], 35 | "provisioners": [ 36 | { 37 | "type": "file", 38 | "source": "ami/docker-storage", 39 | "destination": "/tmp/docker-storage" 40 | }, 41 | { 42 | "type": "file", 43 | "source": "ami/claudia.tar.gz", 44 | "destination": "/tmp/claudia.tar.gz" 45 | }, 46 | { 47 | "type": "file", 48 | "source": "ami/docker-compose.yml", 49 | "destination": "/home/ec2-user/docker-compose.yml" 50 | }, 51 | { 52 | "type": "file", 53 | "source": "config.env", 54 | "destination": "/home/ec2-user/config.env" 55 | }, 56 | { 57 | "type": "shell", 58 | "inline": [ 59 | "sudo yum update -y", 60 | "sudo pvcreate /dev/xvdl", 61 | "sudo vgcreate vgclaudia /dev/xvdl", 62 | "sudo lvcreate --wipesignatures y -l 100%VG -n lvclaudia vgclaudia", 63 | "sudo mkfs -t ext4 /dev/vgclaudia/lvclaudia", 64 | "sudo sh -c \"echo '/dev/vgclaudia/lvclaudia /mnt/claudia ext4 defaults,noatime 0 0' >> /etc/fstab\"", 65 | "sudo mkdir /mnt/claudia", 66 | "sudo mount /mnt/claudia", 67 | "sudo yum install -y docker", 68 | "sudo cp /tmp/docker-storage /etc/sysconfig/docker-storage", 69 | "sudo rm -f /tmp/docker-storage", 70 | "sudo usermod -a -G docker ec2-user", 71 | "sudo rm -rf /var/lib/docker/*", 72 | "sudo service docker start", 73 | "until sudo docker info; do sleep 3; done", 74 | "sudo docker load -i /tmp/claudia.tar.gz", 75 | "sudo rm -f /tmp/claudia.tar.gz", 76 | "sudo curl -L \"https://github.com/docker/compose/releases/download/1.11.1/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/bin/docker-compose", 77 | "sudo chmod ugo+x /usr/bin/docker-compose", 78 | "sudo docker network create ec2user_default", 79 | "sudo docker-compose -f /home/ec2-user/docker-compose.yml create", 80 | "sudo rm -f /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys", 81 | "sudo rm -f /root/docker/config.json /home/*/docker/config.json", 82 | "sudo rm -f /root/.*history /home/*/.*history" 83 | ] 84 | }] 85 | } 86 | -------------------------------------------------------------------------------- /routers/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Applatix, Inc. 2 | package routers 3 | 4 | import ( 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/applatix/claudia/server" 9 | "github.com/applatix/claudia/userdb" 10 | "github.com/applatix/claudia/util" 11 | ) 12 | 13 | // authIdentityHandler is a HTTP handler which simply validates & decodes the session cooke to return current logged in user 14 | func authIdentityHandler(sc *server.ServerContext) func(http.ResponseWriter, *http.Request) { 15 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | si, err := sc.SessionManager.ValidateSession(w, r) 17 | if err != nil { 18 | return 19 | } 20 | identity := make(map[string]interface{}, 0) 21 | identity["username"] = si.Username 22 | util.SuccessHandler(identity, w) 23 | }) 24 | } 25 | 26 | // authIdentityHandler is a HTTP handler which authenticates a user and sets a new session cookie 27 | func authLoginHandler(sc *server.ServerContext) func(http.ResponseWriter, *http.Request) { 28 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 29 | decoder := json.NewDecoder(r.Body) 30 | var reqUser userdb.User 31 | err := decoder.Decode(&reqUser) 32 | if util.ErrorHandler(err, w) != nil { 33 | return 34 | } 35 | defer r.Body.Close() 36 | dbUser, err := sc.UserDB.AuthenticateUser(reqUser.Username, reqUser.Password) 37 | if util.ErrorHandler(err, w) != nil { 38 | return 39 | } 40 | err = sc.SessionManager.SetSession(dbUser, w, r) 41 | if util.ErrorHandler(err, w) != nil { 42 | return 43 | } 44 | util.SuccessHandler(dbUser, w) 45 | }) 46 | } 47 | 48 | // authIdentityHandler is a HTTP handler which will delete and expire the user session cookie 49 | func authLogoutHandler(sc *server.ServerContext) func(http.ResponseWriter, *http.Request) { 50 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 51 | sc.SessionManager.DeleteSession(w, r) 52 | util.SuccessHandler(nil, w) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # @AngularClass 2 | # http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 4 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | insert_final_newline = false 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Users Environment Variables 25 | .lock-wscript 26 | 27 | # OS generated files # 28 | .DS_Store 29 | ehthumbs.db 30 | Icon? 31 | Thumbs.db 32 | 33 | # Node Files # 34 | /node_modules 35 | /bower_components 36 | npm-debug.log 37 | 38 | # Coverage # 39 | /coverage/ 40 | 41 | # Typing # 42 | /src/typings/tsd/ 43 | /typings/ 44 | /tsd_typings/ 45 | 46 | # Dist # 47 | /dist 48 | /public/__build__/ 49 | /src/*/__build__/ 50 | /__build__/** 51 | /public/dist/ 52 | /src/*/dist/ 53 | /dist/** 54 | /.awcache 55 | .webpack.json 56 | 57 | # Doc # 58 | /doc/ 59 | 60 | # IDE # 61 | .idea/ 62 | *.swp 63 | 64 | 65 | !icons/ 66 | -------------------------------------------------------------------------------- /ui/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "indent": 4, 5 | "latedef": true, 6 | "maxlen": 180, 7 | "newcap": true, 8 | "quotmark": "single", 9 | "strict": true, 10 | "undef": true, 11 | "unused": true, 12 | "eqnull": true, 13 | 14 | "eqeqeq": false, 15 | "loopfunc": true, 16 | "camelcase": false 17 | } -------------------------------------------------------------------------------- /ui/.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | # Builds a Docker to deliver dist/ 2 | FROM nginx:latest 3 | COPY dist/ /usr/share/nginx/html -------------------------------------------------------------------------------- /ui/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This script is intended to be run inside the builder container 3 | 4 | set -e 5 | SRCROOT=`dirname $0` 6 | SRCROOT=`cd $SRCROOT;pwd` 7 | 8 | # Tried to be fancy by manipulating NODE_PATH before running the build, but some utilities (e.g webpack) 9 | # as well as npm installed asset files aren't able to resolve libraries and asset locations cleanly. 10 | # For now, we blow away any node_modules directory in the ui dir, and symlink the node_modules that are 11 | # already baked into this container. 12 | #export NODE_PATH="/usr/lib/node_modules/claudia/node_modules" 13 | #export PATH="/usr/lib/node_modules/claudia/node_modules/.bin:$PATH" 14 | cd $SRCROOT 15 | rm -rf $SRCROOT/node_modules 16 | ln -s /root/node/node_modules 17 | npm run build:prod 18 | -------------------------------------------------------------------------------- /ui/config/head-config.common.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | link: [ 3 | 4 | ], 5 | meta: [ 6 | ] 7 | }; 8 | -------------------------------------------------------------------------------- /ui/config/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | // Helper functions 6 | var ROOT = path.resolve(__dirname, '..'); 7 | 8 | function hasProcessFlag(flag) { 9 | return process.argv.join('').indexOf(flag) > -1; 10 | } 11 | 12 | function isWebpackDevServer() { 13 | return process.argv[1] && !! (/webpack-dev-server/.exec(process.argv[1])); 14 | } 15 | 16 | function root(args) { 17 | args = Array.prototype.slice.call(arguments, 0); 18 | return path.join.apply(path, [ROOT].concat(args)); 19 | } 20 | 21 | exports.hasProcessFlag = hasProcessFlag; 22 | exports.isWebpackDevServer = isWebpackDevServer; 23 | exports.root = root; 24 | -------------------------------------------------------------------------------- /ui/config/modules/angular2-hmr-prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.HmrState = function() { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /ui/config/protractor.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('ts-node/register'); 4 | var helpers = require('./helpers'); 5 | 6 | exports.config = { 7 | baseUrl: 'http://localhost:3000/', 8 | 9 | // use `npm run e2e` 10 | specs: [ 11 | helpers.root('src/**/**.e2e.ts'), 12 | helpers.root('src/**/*.e2e.ts') 13 | ], 14 | exclude: [], 15 | 16 | framework: 'jasmine2', 17 | 18 | allScriptsTimeout: 110000, 19 | 20 | jasmineNodeOpts: { 21 | showTiming: true, 22 | showColors: true, 23 | isVerbose: false, 24 | includeStackTrace: false, 25 | defaultTimeoutInterval: 400000 26 | }, 27 | directConnect: true, 28 | 29 | capabilities: { 30 | 'browserName': 'chrome', 31 | 'chromeOptions': { 32 | 'args': ['show-fps-counter=true'] 33 | } 34 | }, 35 | 36 | onPrepare: function () { 37 | browser.ignoreSynchronization = true; 38 | }, 39 | 40 | useAllAngular2AppRoots: true 41 | }; 42 | -------------------------------------------------------------------------------- /ui/config/spec-bundle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Error.stackTraceLimit = Infinity; 4 | 5 | require('core-js/es6'); 6 | require('core-js/es7/reflect'); 7 | 8 | // Typescript emit helpers polyfill 9 | require('ts-helpers'); 10 | 11 | require('zone.js/dist/zone'); 12 | require('zone.js/dist/long-stack-trace-zone'); 13 | require('zone.js/dist/proxy'); // since zone.js 0.6.15 14 | require('zone.js/dist/sync-test'); 15 | require('zone.js/dist/jasmine-patch'); // put here since zone.js 0.6.14 16 | require('zone.js/dist/async-test'); 17 | require('zone.js/dist/fake-async-test'); 18 | 19 | // RxJS 20 | require('rxjs/Rx'); 21 | 22 | var testing = require('@angular/core/testing'); 23 | var browser = require('@angular/platform-browser-dynamic/testing'); 24 | 25 | testing.TestBed.initTestEnvironment( 26 | browser.BrowserDynamicTestingModule, 27 | browser.platformBrowserDynamicTesting() 28 | ); 29 | 30 | /* 31 | * Ok, this is kinda crazy. We can use the context method on 32 | * require that webpack created in order to tell webpack 33 | * what files we actually want to require or import. 34 | * Below, context will be a function/object with file names as keys. 35 | * Using that regex we are saying look in ../src then find 36 | * any file that ends with spec.ts and get its path. By passing in true 37 | * we say do this recursively 38 | */ 39 | var testContext = require.context('../src', true, /\.spec\.ts/); 40 | 41 | /* 42 | * get all the files, for each file, call the context function 43 | * that will require the file and load it up here. Context will 44 | * loop and require those spec files here 45 | */ 46 | function requireAll(requireContext) { 47 | return requireContext.keys().map(requireContext); 48 | } 49 | 50 | // requires and returns all modules that match 51 | var modules = requireAll(testContext); 52 | -------------------------------------------------------------------------------- /ui/karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: @AngularClass 3 | */ 4 | 5 | // Look in ./config for karma.conf.js 6 | module.exports = require('./config/karma.conf.js'); 7 | -------------------------------------------------------------------------------- /ui/protractor.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author: @AngularClass 3 | */ 4 | 5 | // look in ./config for protractor.conf.js 6 | exports.config = require('./config/protractor.conf.js').config; 7 | -------------------------------------------------------------------------------- /ui/src/app/+user-management/change-password/change-password-data.ts: -------------------------------------------------------------------------------- 1 | export class ChangePasswordData { 2 | currentPassword: string; 3 | newPassword: string; 4 | confirmPassword: string; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/app/+user-management/change-password/change-password.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 6 |
7 |
8 | Current Password is required 9 |
10 |
11 |
12 |
13 | 14 | 16 |
17 |
18 | New Password is required 19 |
20 |
21 | Password should contain at least 8 characters. 22 |
23 |
24 |
25 |
26 | 27 | 29 |
30 |
31 | Password confirmation is required 32 |
33 |
34 |
35 | Your password and confirmation password do not match 36 |
37 |
38 |
39 | 40 |
41 |
42 | -------------------------------------------------------------------------------- /ui/src/app/+user-management/change-password/change-password.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { UserService } from '../../services'; 4 | import { AppError } from '../../common/shared'; 5 | import { NotificationsService } from '../../common/notifications'; 6 | import { ChangePasswordData } from './change-password-data'; 7 | 8 | @Component({ 9 | selector: 'axc-change-password', 10 | templateUrl: './change-password.component.html', 11 | }) 12 | export class ChangePasswordComponent { 13 | 14 | public changePasswordData: ChangePasswordData; 15 | 16 | constructor(private userService: UserService, private notificationService: NotificationsService) { 17 | this.changePasswordData = new ChangePasswordData(); 18 | } 19 | 20 | public async doChangePassword(form) { 21 | await this.userService.changePassword(this.changePasswordData).then((res) => { 22 | this.notificationService.success('Password has been updated'); 23 | this.changePasswordData = new ChangePasswordData(); 24 | form.reset(); 25 | }).catch((err) => { 26 | let appErr = err; 27 | this.notificationService.error(appErr.message || 'Can not update password. Please try again later.'); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/app/+user-management/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-management.module'; 2 | -------------------------------------------------------------------------------- /ui/src/app/+user-management/user-details/user-details.component.html: -------------------------------------------------------------------------------- 1 |

Foo User

2 | -------------------------------------------------------------------------------- /ui/src/app/+user-management/user-details/user-details.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/css/config'; 2 | 3 | .user-details { 4 | 5 | &__title-row { 6 | axc-avatar { 7 | display: inline-block; 8 | vertical-align: top; 9 | padding-top: 4px; 10 | } 11 | 12 | h3 { 13 | display: inline-block; 14 | margin: 0; 15 | line-height: 2em; 16 | 17 | i { 18 | color: $ax-color-gray; 19 | font-size: 1em; 20 | padding-left: 0.5em; 21 | } 22 | } 23 | 24 | .editNameForm { 25 | display: inline-block; 26 | line-height: 2em; 27 | } 28 | } 29 | 30 | &__enable-slide { 31 | padding-right: 7.14em; 32 | padding-top: 1em; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/app/+user-management/user-details/user-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-user-details', 5 | templateUrl: './user-details.component.html', 6 | styles: [require('./user-details.component.scss')], 7 | }) 8 | export class UserDetailsComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/app/+user-management/user-management.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 3 | import { NgModule } from '@angular/core'; 4 | import { RouterModule } from '@angular/router'; 5 | 6 | import { ChangePasswordComponent } from './change-password/change-password.component'; 7 | import { UserDetailsComponent } from './user-details/user-details.component'; 8 | import { AxLibModule, AxcCommonModule } from '../common'; 9 | import { CollapseBoxModule } from '../common/collapse-box'; 10 | import { ValidateOnBlurDirective } from '../common/validate-onblur'; 11 | import { AxcPipeModule } from '../pipe/axc-pipe.module'; 12 | 13 | export const routes = [ 14 | { path: '', component: ChangePasswordComponent, data: { title: 'Users' } }, 15 | { path: 'change-password', component: ChangePasswordComponent, data: { title: 'Change Password' } }, 16 | { path: 'user-details/:id', component: UserDetailsComponent, data: { title: 'User details' } }, 17 | ]; 18 | 19 | @NgModule({ 20 | declarations: [ 21 | ChangePasswordComponent, 22 | UserDetailsComponent, 23 | ValidateOnBlurDirective, 24 | ], 25 | imports: [ 26 | AxcPipeModule, 27 | AxLibModule, 28 | AxcCommonModule, 29 | CollapseBoxModule, 30 | CommonModule, 31 | FormsModule, 32 | ReactiveFormsModule, 33 | RouterModule.forChild(routes), 34 | ], 35 | }) 36 | export default class UserManagementModule { 37 | 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/app/accounts/accounts.component.html: -------------------------------------------------------------------------------- 1 | 2 |

Account Alias Names

3 |

4 | No accounts are available to alias. Please complete report setup and accounts will start appearing as data processing completes. 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 |
AWS Account IDAlias name
{{ account.aws_account_id }} 17 |
18 | 19 | 20 |
21 |
22 | 23 | Save 24 |
25 |
29 | -------------------------------------------------------------------------------- /ui/src/app/accounts/accounts.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../assets/css/config'; 2 | 3 | .input-edit { 4 | padding: 0; 5 | color: $ax-color-gray-7; 6 | background: transparent; 7 | border-color: transparent; 8 | outline: 0; 9 | 10 | &:not([readonly]) { 11 | padding: 8px; 12 | background: $ax-color-gray-1; 13 | border: 1px solid $ax-color-gray-4; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/app/accounts/accounts.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewEncapsulation } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | import { Report, Account } from '../model'; 5 | import { ReportingService, ConfigService } from '../services'; 6 | import { NotificationsService } from '../common/notifications/notifications.service'; 7 | 8 | @Component({ 9 | selector: 'axc-accounts', 10 | templateUrl: './accounts.component.html', 11 | encapsulation: ViewEncapsulation.None, 12 | styles: [ 13 | require('./accounts.component.scss'), 14 | ], 15 | }) 16 | export class AccountsComponent implements OnInit { 17 | 18 | public report: Report; 19 | public editedAccount: Account; 20 | 21 | constructor( 22 | private route: ActivatedRoute, 23 | private notificationsService: NotificationsService, 24 | private reportingService: ReportingService, 25 | private configService: ConfigService, 26 | ) { } 27 | 28 | public async ngOnInit() { 29 | this.report = await this.configService.getReportConfig(); 30 | } 31 | 32 | public editAccount(account: Account) { 33 | this.editedAccount = account; 34 | } 35 | 36 | public saveAccount(index: number) { 37 | let newAccountData = this.editedAccount; 38 | this.reportingService.saveAccountName(this.report.id, this.editedAccount.aws_account_id, { name: this.editedAccount.name }) 39 | .then(() => { 40 | this.report.accounts[index] = newAccountData; 41 | this.editedAccount = null; 42 | this.notificationsService.success('Account alias is updated.'); 43 | }, () => { 44 | this.notificationsService.error('Something went wrong.'); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/app/accounts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './accounts.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ui/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-app', 5 | styles: [ 6 | require('assets/css/main.scss'), 7 | ], 8 | encapsulation: ViewEncapsulation.None, 9 | templateUrl: 'app.component.html', 10 | }) 11 | export class AppComponent { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/app/app.error-handler.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ErrorHandler, Injector } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { ERROR_CODES } from './common/shared'; 4 | 5 | @Injectable() 6 | export class AppErrorHandler extends ErrorHandler { 7 | private cachedRouter: Router; 8 | 9 | constructor(private injector: Injector) { 10 | super(false); 11 | } 12 | 13 | public handleError(error: any): void { 14 | if (error.rejection && error.rejection.code) { 15 | switch (error.rejection.code) { 16 | case ERROR_CODES.UNAUTHENTICATED_REQUEST: 17 | this.process401Error(); 18 | break; 19 | case ERROR_CODES.UNAUTHORIZED_REQUEST: 20 | this.process403Error(); 21 | break; 22 | case ERROR_CODES.ENTITY_NOT_FOUND: 23 | this.process404Error(); 24 | break; 25 | default: 26 | console.error(error); 27 | } 28 | } else { 29 | if (error.rejection) { 30 | let rejection = error.rejection; 31 | console.error('Unhandled Promise rejection:', 32 | rejection instanceof Error ? rejection.message : rejection, 33 | '; Zone:', error.zone.name, 34 | '; Task:', error.task && error.task.source, 35 | '; Value:', rejection, rejection instanceof Error ? rejection.stack : undefined); 36 | } else { 37 | super.handleError(error); 38 | } 39 | } 40 | } 41 | 42 | private get router(): Router { 43 | if (!this.cachedRouter) { 44 | this.cachedRouter = this.injector.get(Router); 45 | } 46 | return this.cachedRouter; 47 | } 48 | /** 49 | * We wrote our setTimeout call to redirect 50 | * We are pretty much outside of ngZone - this will bring us back into game on that. 51 | */ 52 | private doRedirect(url) { 53 | setTimeout(() => { 54 | this.router.navigateByUrl(url); 55 | }, 1); 56 | } 57 | /** 58 | * Specific handler for taking care of 401 error code if needed 59 | */ 60 | private process401Error() { 61 | if (window.location.pathname.indexOf('login') === -1) { 62 | let path = window.location.pathname; 63 | this.doRedirect('/base/login/' + encodeURIComponent(path)); 64 | } 65 | } 66 | 67 | /** 68 | * Specific handler for taking care of 403 error code if needed 69 | */ 70 | private process403Error() { 71 | this.doRedirect('/error/403/type/' + ERROR_CODES.UNAUTHORIZED_REQUEST); 72 | } 73 | 74 | /** 75 | * Specific handler for taking care of 404 error code if needed 76 | */ 77 | private process404Error() { 78 | this.doRedirect('/error/404/type/' + ERROR_CODES.ENTITY_NOT_FOUND); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ui/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | import { LoginComponent } from './login'; 4 | import { BaseLayoutComponent, IntroLayoutComponent } from './layout'; 5 | import { ErrorPageComponent } from './error-page/error-page.component'; 6 | import { WhoAmIResolver } from './resolvers/who-am-i.resolver'; 7 | import { HasNoSessionResolver } from './resolvers/has-no-session.resolver'; 8 | import { ReportResolver } from './resolvers/report.resolver'; 9 | import { ReportComponent } from './report'; 10 | import { DashboardComponent } from './dashboard'; 11 | import { ConfigComponent } from './config'; 12 | import { AccountsComponent } from './accounts'; 13 | import { EulaComponent } from './eula'; 14 | import { SettingsComponent } from './settings'; 15 | 16 | export const ROUTES: Routes = [ 17 | { 18 | path: 'app', 19 | component: BaseLayoutComponent, 20 | resolve: { 21 | whoAmIResolver: WhoAmIResolver, 22 | }, 23 | children: [ 24 | { 25 | path: 'users', loadChildren: () => System.import('./+user-management').then((comp: any) => { 26 | return comp.default; 27 | }), 28 | data: { title: 'Users' }, 29 | }, 30 | { 31 | path: 'report', component: ReportComponent, resolve: { 32 | report: ReportResolver, 33 | }, 34 | }, 35 | { 36 | path: 'accounts', component: AccountsComponent, resolve: { 37 | report: ReportResolver, 38 | }, 39 | }, 40 | { 41 | path: 'dashboard', component: DashboardComponent, 42 | }, 43 | { 44 | path: 'config', component: SettingsComponent, 45 | }, 46 | { 47 | path: 'settings', component: SettingsComponent, 48 | }, 49 | ], 50 | }, 51 | { 52 | path: 'base', 53 | component: IntroLayoutComponent, 54 | children: [ 55 | { 56 | path: 'login/:fwd', component: LoginComponent, resolve: { 57 | hasNoSessionResolver: HasNoSessionResolver, 58 | }, 59 | }, 60 | { 61 | path: 'login', component: LoginComponent, resolve: { 62 | hasNoSessionResolver: HasNoSessionResolver, 63 | }, 64 | }, 65 | { 66 | path: 'eula', component: EulaComponent, 67 | resolve: { 68 | whoAmIResolver: WhoAmIResolver, 69 | }, 70 | }, 71 | ], 72 | }, 73 | { path: 'error/:code', component: ErrorPageComponent }, 74 | { path: 'error/:code/type/:type', component: ErrorPageComponent }, 75 | { path: '**', redirectTo: 'base/login' }, 76 | ]; 77 | -------------------------------------------------------------------------------- /ui/src/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | export type InternalStateType = { 4 | [key: string]: any 5 | }; 6 | 7 | @Injectable() 8 | export class AppState { 9 | private appState: InternalStateType = { }; 10 | 11 | get state() { 12 | return this.appState = this._clone(this.appState); 13 | } 14 | 15 | set state(value) { 16 | throw new Error('do not mutate the `.state` directly'); 17 | } 18 | 19 | public get(prop?: any) { 20 | // use our state getter for the clone 21 | const state = this.state; 22 | return state.hasOwnProperty(prop) ? state[prop] : state; 23 | } 24 | 25 | public set(prop: string, value: any) { 26 | // internally mutate our state 27 | return this.appState[prop] = value; 28 | } 29 | 30 | private _clone(object: InternalStateType) { 31 | // simple object clone 32 | return JSON.parse(JSON.stringify( object )); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/app/common/abstract-value-accessor/abstract-value-accessor.ts: -------------------------------------------------------------------------------- 1 | import { ControlValueAccessor } from '@angular/forms'; 2 | 3 | export abstract class AbstractValueAccessor implements ControlValueAccessor { 4 | 5 | private newValue: any = ''; 6 | 7 | get value(): any { 8 | return this.newValue; 9 | }; 10 | 11 | set value(v: any) { 12 | if (v !== this.newValue) { 13 | this.newValue = v; 14 | this.onChange(v); 15 | } 16 | } 17 | 18 | public writeValue(value: any) { 19 | this.newValue = value; 20 | this.onChange(value); 21 | } 22 | 23 | public onChange = (_) => {}; // tslint:disable-line 24 | 25 | public onTouched = () => {}; // tslint:disable-line 26 | 27 | public registerOnChange(fn: (_: any) => void): void { 28 | this.onChange = fn; 29 | } 30 | 31 | public registerOnTouched(fn: () => void): void { 32 | this.onTouched = fn; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/ax-lib.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { CheckboxComponent } from './checkbox'; 6 | import { CheckboxSlideComponent } from './checkbox-slide'; 7 | import { DropDownComponent, DropdownContentDirective, DropdownAnchorDirective } from './dropdown/dropdown.component'; 8 | import { DateRangeComponent } from './date-range'; 9 | import { MultiselectDropdownComponent } from './dropdown-multiselect/dropdown-multiselect.component'; 10 | import { MultiSelectSearchFilter } from './dropdown-multiselect/dropdown-multiselect.component'; 11 | import { RadioButtonComponent } from './radio-button'; 12 | import { SlidingPanelComponent } from './sliding-panel/sliding-panel.component'; 13 | import { HttpService } from './services'; 14 | import { SortArrowComponent } from './sort-arrow/sort-arrow.component'; 15 | import { HelpPanelComponent } from './help-panel/help-panel.component'; 16 | import { SwitchComponent } from './switch/switch.component'; 17 | 18 | @NgModule({ 19 | declarations: [ 20 | CheckboxComponent, 21 | CheckboxSlideComponent, 22 | DropDownComponent, 23 | MultiselectDropdownComponent, 24 | MultiSelectSearchFilter, 25 | RadioButtonComponent, 26 | SlidingPanelComponent, 27 | SortArrowComponent, 28 | HelpPanelComponent, 29 | DropdownContentDirective, 30 | DropdownAnchorDirective, 31 | DateRangeComponent, 32 | SwitchComponent, 33 | ], 34 | imports: [ 35 | CommonModule, 36 | FormsModule, 37 | ], 38 | exports: [ 39 | CheckboxComponent, 40 | CheckboxSlideComponent, 41 | DropDownComponent, 42 | MultiselectDropdownComponent, 43 | RadioButtonComponent, 44 | SlidingPanelComponent, 45 | SortArrowComponent, 46 | HelpPanelComponent, 47 | DropdownContentDirective, 48 | DropdownAnchorDirective, 49 | DateRangeComponent, 50 | SwitchComponent, 51 | ], 52 | providers: [ 53 | HttpService, 54 | ], 55 | }) 56 | export class AxLibModule { 57 | } 58 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/checkbox-slide/checkbox-slide.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/checkbox-slide/checkbox-slide.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/css/config'; 2 | 3 | .ax-checkbox-slide { 4 | position: relative; 5 | display: inline-block; 6 | 7 | input { 8 | position: absolute; 9 | width: 30px; 10 | height: 20px; 11 | z-index: 1; 12 | opacity: 0; 13 | cursor: pointer; 14 | 15 | & + span { 16 | position: relative; 17 | top: 3px; 18 | display: inline-block; 19 | width: 30px; 20 | height: 14px; 21 | margin-right: 6px; 22 | background-color: $ax-color-bg; 23 | border-radius: 7px; 24 | box-shadow: inset 0px 1px 2px rgba(#000, .2); 25 | 26 | &::before { 27 | position: absolute; 28 | top: -2px; 29 | left: -2px; 30 | display: inline-block; 31 | vertical-align: middle; 32 | width: 18px; 33 | height: 18px; 34 | background-color: $ax-color-bg-light; 35 | border-radius: 50%; 36 | box-shadow: 1px 1px 2px #999; 37 | transition: left .2s; 38 | content: ''; 39 | } 40 | } 41 | 42 | &:checked + span { 43 | background-color: rgba($ax-color-base-dlight, .3); 44 | 45 | &::before { 46 | left: 16px; 47 | background-color: $ax-color-base-dlight; 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/checkbox-slide/checkbox-slide.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, forwardRef } from '@angular/core'; 2 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | 4 | import { AbstractValueAccessor } from '../../abstract-value-accessor/abstract-value-accessor'; 5 | 6 | const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR = { 7 | provide: NG_VALUE_ACCESSOR, 8 | useExisting: forwardRef(() => CheckboxSlideComponent), // tslint:disable-line 9 | multi: true, 10 | }; 11 | 12 | @Component({ 13 | selector: 'axc-checkbox-slide', 14 | templateUrl: './checkbox-slide.component.html', 15 | styles: [require('./checkbox-slide.component.scss')], 16 | providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR], 17 | }) 18 | export class CheckboxSlideComponent extends AbstractValueAccessor { 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/checkbox-slide/index.ts: -------------------------------------------------------------------------------- 1 | export * from './checkbox-slide.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/checkbox/checkbox.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | L 5 | 6 |
7 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/checkbox/checkbox.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/css/config'; 2 | 3 | .ax-checkbox { 4 | position: relative; 5 | display: inline-block; 6 | line-height: 18px; 7 | 8 | input { 9 | position: absolute; 10 | z-index: 1; 11 | width: 18px; 12 | height: 18px; 13 | opacity: 0; 14 | cursor: pointer; 15 | 16 | & + .ax-checkbox__box { 17 | display: inline-block; 18 | vertical-align: middle; 19 | width: 18px; 20 | height: 18px; 21 | text-align: center; 22 | border-radius: 3px; 23 | border: 2px solid #999; 24 | transition: background-color .2s; 25 | 26 | .ax-checkbox__mark { 27 | position: relative; 28 | top: -3px; 29 | transform: scaleX(-1) rotate(-35deg); 30 | display: inline-block; 31 | visibility: hidden; 32 | font-size: 15px; 33 | font-family: 'Arial', sans-serif; 34 | color: $ax-color-bg-light; 35 | } 36 | } 37 | 38 | &:checked + .ax-checkbox__box { 39 | color: $ax-color-bg-light; 40 | background-color: $ax-color-base-dlight; 41 | border-color: transparent; 42 | 43 | .ax-checkbox__mark { 44 | visibility: visible; 45 | } 46 | 47 | } 48 | } 49 | 50 | &:not(:last-child) { 51 | input + span { 52 | margin-right: 6px; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/checkbox/checkbox.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, forwardRef, Input } from '@angular/core'; 2 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | 4 | import { AbstractValueAccessor } from '../../abstract-value-accessor/abstract-value-accessor'; 5 | 6 | const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR = { 7 | provide: NG_VALUE_ACCESSOR, 8 | useExisting: forwardRef(() => CheckboxComponent), // tslint:disable-line 9 | multi: true, 10 | }; 11 | 12 | @Component({ 13 | selector: 'axc-checkbox', 14 | templateUrl: './checkbox.component.html', 15 | styles: [require('./checkbox.component.scss')], 16 | providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR], 17 | }) 18 | export class CheckboxComponent extends AbstractValueAccessor { 19 | 20 | @Input() 21 | public disabled: boolean = false; 22 | 23 | @Input() 24 | set value(v: any) { 25 | super.value = v; 26 | } 27 | 28 | get value(): any { 29 | return super.value; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './checkbox.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/date-range/_date-range.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/css/config'; 2 | 3 | .date-range-selector { 4 | display: block; 5 | width: 100%; 6 | padding: 0 16px; 7 | font-weight: 500; 8 | font-size: .875em; 9 | line-height: 40px; 10 | color: $ax-color-teal-7; 11 | text-transform: uppercase; 12 | background-color: $ax-color-gray-2; 13 | border-radius: 5px; 14 | cursor: pointer; 15 | outline: 0; 16 | 17 | &:hover { 18 | background-color: $ax-color-gray-1; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/date-range/date-range.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter, ElementRef, OnInit } from '@angular/core'; 2 | import { DateRange } from './date-range'; 3 | 4 | @Component({ 5 | selector: 'ax-date-range', 6 | templateUrl: './date-range.html', 7 | styles: [ 8 | require('./_date-range.scss'), 9 | ], 10 | }) 11 | export class DateRangeComponent implements OnInit { 12 | 13 | @Input() 14 | public range: DateRange; 15 | 16 | @Output() 17 | public rangeChanged: EventEmitter = new EventEmitter(); 18 | 19 | private target; 20 | 21 | constructor(private el: ElementRef) { 22 | } 23 | 24 | public ngOnInit() { 25 | let that = this; 26 | this.target = $(this.el.nativeElement).find('.date-range-selector'); 27 | this.target.dateRangePicker({ 28 | showShortcuts: true, autoClose: true, 29 | shortcuts: { 30 | 'prev-days': [3, 7, 30], 31 | prev: ['week', 'month', 'year'], 32 | 'next-days': null, 33 | next: null, 34 | }, 35 | }).bind('datepicker-change', (evt, obj) => { 36 | window.setTimeout(() => { 37 | that.updateRange(obj); 38 | }, 1); 39 | }); 40 | 41 | this.updatePicker(); 42 | } 43 | 44 | private updatePicker() { 45 | this.target.data('dateRangePicker').setDateRange(this.range.startDate, this.range.endDate, true); 46 | } 47 | 48 | private updateRange(obj) { 49 | this.range.setRange(obj.date1, obj.date2); 50 | this.rangeChanged.next(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/date-range/date-range.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/date-range/date-range.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | 3 | export class DateRange { 4 | private _startDate: moment.Moment; 5 | private _endDate: moment.Moment; 6 | private dateFormat: string = 'YYYY-MM-DD'; 7 | 8 | constructor(startDate: string, endDate: string) { 9 | if (!startDate || !endDate) { 10 | throw 'Date range needs start and end dates'; 11 | } 12 | this.startDate = startDate; 13 | this.endDate = endDate; 14 | } 15 | 16 | get startDate(): string { 17 | return this._startDate.format(this.dateFormat); 18 | } 19 | set startDate(val: string) { 20 | this._startDate = moment(val, this.dateFormat); 21 | } 22 | 23 | get endDate(): string { 24 | return this._endDate.format(this.dateFormat); 25 | } 26 | set endDate(val: string) { 27 | this._endDate = moment(val, this.dateFormat); 28 | } 29 | 30 | public setRange(startDate: Date, endDate: Date) { 31 | this._endDate = moment(this.getDateString(endDate), 'YYYY-MM-DD'); 32 | this._startDate = moment(this.getDateString(startDate), 'YYYY-MM-DD'); 33 | } 34 | 35 | public format(): string { 36 | let dateFormat = 'D MMM YYYY'; 37 | return `${this._startDate.format(dateFormat)} - ${this._endDate.format(dateFormat)}`; 38 | } 39 | 40 | private getDateString(date: Date): string { 41 | return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/date-range/index.ts: -------------------------------------------------------------------------------- 1 | export { DateRangeComponent } from './date-range.component'; 2 | export { DateRange } from './date-range'; 3 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/dropdown-multiselect/dropdown-multiselect.html: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/dropdown-multiselect/dropdown-multiselect.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/css/config'; 2 | 3 | .dropdown-multiselect { 4 | a { outline: none !important; } 5 | 6 | height: 37px; 7 | width: 100%; 8 | max-width: 100%; 9 | padding: 8px 0; 10 | font-size: 16px; 11 | margin-top: 8px; 12 | border: none; 13 | border-bottom: 1px solid #7c818a; 14 | border-radius: 0; 15 | box-shadow: none !important; 16 | background-color: inherit; 17 | cursor: pointer; 18 | -webkit-appearance: none; 19 | 20 | button { 21 | height: 37px; 22 | width: 100%; 23 | text-align: left; 24 | .caret { 25 | position: absolute; 26 | right: 28px; 27 | margin-top: 8px; 28 | width: 0; 29 | height: 0; 30 | border-left: 6px solid transparent; 31 | border-right: 6px solid transparent; 32 | border-top: 6px solid #777; 33 | } 34 | } 35 | 36 | .dropdown-menu { 37 | background-color: $ax-color-bg-light; 38 | border: 2px solid $ax-color-bg-strong; 39 | z-index: $ax-zIndex-dropdown-menu; 40 | margin-left: 0px; 41 | position: absolute; 42 | min-width: 200px; 43 | 44 | li { 45 | margin: 10px; 46 | } 47 | } 48 | 49 | &:focus { 50 | box-shadow: none; 51 | border-color: $ax-color-base; 52 | } 53 | 54 | .ax-btn { 55 | padding: 0 0.5em; 56 | } 57 | 58 | } 59 | 60 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/dropdown/_dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown__anchor { 2 | cursor: pointer; 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | } 6 | 7 | .dropdown-pane { 8 | box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.2); 9 | 10 | &--menu { 11 | padding: 0; 12 | 13 | & .ax-dropdown-content > ul { 14 | margin: 0; 15 | list-style-type: none; 16 | white-space: nowrap; 17 | text-align: left; 18 | cursor: pointer; 19 | min-width: 150px; 20 | 21 | li { 22 | padding: 0.5em 1em; 23 | border-bottom: 1px solid #cacaca; 24 | color: #3E3E3E; 25 | cursor: pointer; 26 | 27 | &:hover { 28 | background-color: #eee; 29 | } 30 | 31 | &:last-child { 32 | border-bottom: none; 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/help-panel/help-panel.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 |
7 |

Applatix help

8 |
9 |
10 | 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/help-panel/help-panel.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/css/config'; 2 | 3 | .help-panel { 4 | margin: -20px; 5 | 6 | &__header { 7 | position: relative; 8 | padding: 20px 20px 20px 76px; 9 | color: $ax-color-base-strong; 10 | background-color: $ax-color-base-mlight; 11 | border-bottom: 1px solid $ax-color-base; 12 | 13 | h3 { 14 | margin: 0; 15 | font-weight: bold; 16 | font-size: 18px; 17 | line-height: 36px; 18 | text-transform: uppercase; 19 | } 20 | } 21 | 22 | &__body { 23 | max-width: 780px; 24 | padding: 20px 20px 20px 76px; 25 | } 26 | 27 | &__close { 28 | position: absolute; 29 | left: 20px; 30 | top: 20px; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/help-panel/help-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { SlidingPanelComponent } from '../sliding-panel/sliding-panel.component'; 3 | 4 | @Component({ 5 | selector: 'axc-help-panel', 6 | templateUrl: './help-panel.component.html', 7 | styles: [require('./help-panel.component.scss')], 8 | }) 9 | export class HelpPanelComponent extends SlidingPanelComponent { 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/help-panel/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/app/common/ax-lib/help-panel/index.ts -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/index.ts: -------------------------------------------------------------------------------- 1 | export { DropDownComponent } from './dropdown/dropdown.component'; 2 | export { AxLibModule } from './ax-lib.module'; 3 | export { SlidingPanelComponent } from './sliding-panel/sliding-panel.component'; 4 | export { SortArrowComponent } from './sort-arrow'; 5 | export * from './services'; 6 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/radio-button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './radio-button.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/radio-button/radio-button.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/radio-button/radio-button.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/css/config'; 2 | 3 | .ax-radio { 4 | position: relative; 5 | display: inline-block; 6 | 7 | & + label { 8 | cursor: pointer; 9 | } 10 | 11 | input { 12 | position: absolute; 13 | width: 18px; 14 | height: 18px; 15 | opacity: 0; 16 | cursor: pointer; 17 | 18 | & + span { 19 | display: inline-block; 20 | vertical-align: middle; 21 | width: 18px; 22 | height: 18px; 23 | border-radius: 50%; 24 | border: 2px solid #999; 25 | transition: background-color .2s, border-color .2s; 26 | } 27 | 28 | &:checked + span { 29 | background-color: $ax-color-base-dlight; 30 | border-color: $ax-color-base-dlight; 31 | box-shadow: inset 0 0 0 3px $ax-color-bg; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/radio-button/radio-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, forwardRef, Input } from '@angular/core'; 2 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | 4 | import { AbstractValueAccessor } from '../../abstract-value-accessor/abstract-value-accessor'; 5 | 6 | const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR = { 7 | provide: NG_VALUE_ACCESSOR, 8 | useExisting: forwardRef(() => RadioButtonComponent), // tslint:disable-line 9 | multi: true, 10 | }; 11 | 12 | @Component({ 13 | selector: 'axc-radio-button', 14 | templateUrl: './radio-button.component.html', 15 | styles: [require('./radio-button.component.scss')], 16 | providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR], 17 | }) 18 | export class RadioButtonComponent extends AbstractValueAccessor { 19 | 20 | @Input() 21 | public name: string; 22 | 23 | @Input() 24 | public radioValue: string; 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/services/http.service.ts: -------------------------------------------------------------------------------- 1 | import {Observable, Observer} from 'rxjs'; 2 | import {Injectable, NgZone} from '@angular/core'; 3 | 4 | declare let untar; 5 | 6 | interface Callback {(data: any): void; } 7 | 8 | declare class EventSource { 9 | /* tslint:disable */ 10 | constructor(name: string, options?: any); 11 | public onmessage: Callback; 12 | public onerror: Callback; 13 | public close(): void; 14 | /* tslint:enable */ 15 | } 16 | 17 | enum ReadyState { 18 | CONNECTING = 0, 19 | OPEN = 1, 20 | CLOSED = 2, 21 | DONE = 4 22 | } 23 | 24 | /** 25 | * Implements specific low level http requests e.g. reading blob or server sent events. 26 | */ 27 | @Injectable() 28 | export class HttpService { 29 | constructor(private zone: NgZone) {} 30 | 31 | /** 32 | * Loads and unpack tarball. 33 | */ 34 | public loadTar(url): Observable<{name: string, blob: Blob}> { 35 | return Observable.create((observer: Observer<{name: string, blob: Blob}>) => { 36 | let zone = this.zone; 37 | 38 | let xhr = new XMLHttpRequest(); 39 | xhr.onreadystatechange = () => { 40 | if (xhr.readyState === ReadyState.DONE && xhr.status === 200) { 41 | untar(xhr.response).progress(file => { 42 | zone.run(() => observer.next(file)); 43 | }).then(() => { 44 | zone.run(() => observer.complete()); 45 | }).catch(err => { 46 | zone.run(() => observer.error(err)); 47 | }); 48 | } 49 | }; 50 | xhr.open('GET', url); 51 | xhr.responseType = 'arraybuffer'; 52 | xhr.send(); 53 | 54 | return () => { xhr.abort(); }; 55 | }); 56 | } 57 | 58 | /** 59 | * Reads server sent messages from specified URL. 60 | */ 61 | public loadEventSource(url): Observable { 62 | return Observable.create((observer: Observer) => { 63 | let eventSource = new EventSource(url, {withCredentials: true}); 64 | eventSource.onmessage = msg => observer.next(msg.data); 65 | eventSource.onerror = e => { 66 | if (e.eventPhase === ReadyState.CLOSED) { 67 | observer.complete(); 68 | } else { 69 | observer.error(e); 70 | } 71 | } 72 | return () => { 73 | eventSource.close(); 74 | }; 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http.service'; 2 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/sliding-panel/_sliding-panel.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/css/config'; 2 | 3 | $animation-duration: .4s; 4 | $shadow: -4px -4px 6px rgba(0, 0, 0, .2); 5 | 6 | .sliding-panel { 7 | top: 0; 8 | bottom: 0; 9 | position: fixed; 10 | box-shadow: $shadow; 11 | z-index: $ax-zIndex-sliding-panel; 12 | background-color: #FFF; 13 | overflow-y: auto; 14 | padding: 20px; 15 | visibility: hidden; 16 | 17 | &__close-button { 18 | position: absolute; 19 | cursor: pointer; 20 | } 21 | 22 | &--left { 23 | transition: left $animation-duration, visibility $animation-duration; 24 | left: 100%; 25 | right: 0; 26 | } 27 | 28 | &--right { 29 | transition: right $animation-duration, visibility $animation-duration; 30 | left: 0; 31 | right: 100%; 32 | } 33 | 34 | &--opened { 35 | visibility: visible; 36 | } 37 | } 38 | 39 | .sliding-panel--opened.sliding-panel--left { 40 | right: 0; 41 | left: 40%; 42 | 43 | .sliding-panel__close-button { 44 | left: 5px; 45 | top: 3px; 46 | } 47 | } 48 | 49 | .sliding-panel--opened.sliding-panel--right { 50 | right: 20%; 51 | left: 0; 52 | 53 | .sliding-panel__close-button { 54 | right: 5px; 55 | top: 3px; 56 | } 57 | } 58 | 59 | .popup-overlay { 60 | position: fixed; 61 | z-index: 2; 62 | top: 0; 63 | left: 0; 64 | width: 100%; 65 | height: 100%; 66 | background-color: $ax-color-gray-3; 67 | transition: opacity #{$animation-duration}, visibility #{$animation-duration}; 68 | 69 | &:not(.opened) { 70 | opacity: 0; 71 | visibility: hidden; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/sliding-panel/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sliding-panel.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/sliding-panel/sliding-panel.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |
8 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/sliding-panel/sliding-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ax-sliding-panel', 5 | templateUrl: './sliding-panel.component.html', 6 | styles: [require('./_sliding-panel.component.scss')], 7 | }) 8 | export class SlidingPanelComponent { 9 | @Input() 10 | public show: boolean = false; 11 | @Input() 12 | public position: 'left' | 'right' = 'left'; 13 | @Input() 14 | public hasCloseButton: boolean = true; 15 | @Output() 16 | public closeButtonClick: EventEmitter = new EventEmitter(); 17 | 18 | public onCloseButtonClick() { 19 | this.closeButtonClick.emit(this); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/sort-arrow/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sort-arrow.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/sort-arrow/sort-arrow.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/sort-arrow/sort-arrow.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/app/common/ax-lib/sort-arrow/sort-arrow.component.scss -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/sort-arrow/sort-arrow.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-sort-arrow', 5 | templateUrl: './sort-arrow.component.html', 6 | styles: [require('./sort-arrow.component.scss')], 7 | }) 8 | export class SortArrowComponent { 9 | @Input() 10 | private columnName: string; 11 | 12 | @Input() 13 | private sort: string; 14 | 15 | @Input() 16 | private sortBy: string; 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/switch/switch.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-switch', 5 | templateUrl: './switch.html', 6 | styles: [require('./switch.scss')], 7 | }) 8 | export class SwitchComponent { 9 | 10 | @Input() 11 | public options = []; 12 | @Output() 13 | public onOptionChanged: EventEmitter = new EventEmitter(); 14 | @Input() 15 | public selectedValue: string; 16 | 17 | protected selectOption(value: string) { 18 | let changed = value !== this.selectedValue; 19 | this.selectedValue = value; 20 | if (changed) { 21 | this.onOptionChanged.emit(value); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/switch/switch.html: -------------------------------------------------------------------------------- 1 |
2 | 6 |
-------------------------------------------------------------------------------- /ui/src/app/common/ax-lib/switch/switch.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/css/config'; 2 | 3 | .ax-switch { 4 | display: inline-block; 5 | padding: 2px; 6 | background-color: $ax-color-teal-6; 7 | border-radius: 6px; 8 | 9 | &__button { 10 | display: inline-block; 11 | padding: 0 24px; 12 | font-weight: 500; 13 | font-size: .875em; 14 | line-height: 36px; 15 | color: $ax-color-gray-1; 16 | text-transform: uppercase; 17 | border-radius: 5px; 18 | outline: 0; 19 | 20 | &.active { 21 | color: $ax-color-gray-7; 22 | background-color: $ax-color-gray-1; 23 | } 24 | 25 | &:not(.active) { 26 | cursor: pointer; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/app/common/collapse-box/collapse-box.component.html: -------------------------------------------------------------------------------- 1 |
4 |
5 | 9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /ui/src/app/common/collapse-box/collapse-box.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/css/config'; 2 | 3 | .collapse-box { 4 | overflow: hidden; 5 | 6 | &__button { 7 | position: relative; 8 | top: 25px; 9 | right: 20px; 10 | float: right; 11 | font-size: 15px; 12 | } 13 | 14 | &--collapsed { 15 | height: 100px; 16 | } 17 | &--expanded { 18 | height: auto; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/common/collapse-box/collapse-box.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-collapse-box', 5 | templateUrl: './collapse-box.component.html', 6 | styles: [require('./collapse-box.component.scss')], 7 | }) 8 | export class CollapseBoxComponent { 9 | @Input() public collapsed: boolean = false; 10 | 11 | public toggle() { 12 | if (this.collapsed) { 13 | this.show(); 14 | } else { 15 | this.hide(); 16 | } 17 | } 18 | 19 | private hide() { 20 | this.collapsed = true; 21 | } 22 | 23 | private show() { 24 | this.collapsed = false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/common/collapse-box/collapse-box.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { CollapseBoxComponent } from './collapse-box.component'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | CollapseBoxComponent, 10 | ], 11 | imports: [ 12 | RouterModule, 13 | CommonModule, 14 | ], 15 | exports: [ 16 | CollapseBoxComponent, 17 | ], 18 | }) 19 | export class CollapseBoxModule { 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/common/collapse-box/index.ts: -------------------------------------------------------------------------------- 1 | export * from './collapse-box.module'; 2 | -------------------------------------------------------------------------------- /ui/src/app/common/http-interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, XHRBackend, RequestOptions, Request, RequestOptionsArgs, Response, Headers } from '@angular/http'; 3 | import { Observable } from 'rxjs/Rx'; 4 | import { ERROR_CODES, AppError } from '../common/shared'; 5 | 6 | @Injectable() 7 | export class HttpInterceptor extends Http { 8 | protected basePath = `${process.env.API_PROTOCOL}${process.env.API_URI}`; 9 | constructor( 10 | private backend: XHRBackend, 11 | private defaultOptions: RequestOptions) { 12 | super(backend, defaultOptions); 13 | } 14 | 15 | public request(url: string | Request, options?: RequestOptionsArgs): Observable { 16 | if (url instanceof Request) { 17 | url.url = this.basePath + url.url; 18 | } else { 19 | url = this.basePath + url; 20 | } 21 | 22 | options = options || {}; 23 | options.withCredentials = true; 24 | let headers = new Headers(); 25 | headers.set('Content-Type', 'application/json'); 26 | options.headers = headers; 27 | return super.request(url, options).catch((res: Response) => { 28 | try { 29 | // Try to parse REST API error 30 | return Observable.throw(res.json()); 31 | } catch (e) { 32 | // Fallback to HTTP response code if API response does not have properly formatter error 33 | let code = ''; 34 | switch (res.status) { 35 | case 400: 36 | code = ERROR_CODES.INVALID_REQUEST; 37 | break; 38 | case 401: 39 | code = ERROR_CODES.UNAUTHENTICATED_REQUEST; 40 | break; 41 | case 403: 42 | code = ERROR_CODES.UNAUTHORIZED_REQUEST; 43 | break; 44 | case 404: 45 | code = ERROR_CODES.ENTITY_NOT_FOUND; 46 | break; 47 | default: 48 | code = ERROR_CODES.INTERNAL_ERROR; 49 | } 50 | return Observable.throw( { code, message: res.text() }); 51 | } 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ui/src/app/common/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Http, XHRBackend, RequestOptions } from '@angular/http'; 3 | import { AxLibModule } from './ax-lib'; 4 | 5 | import { HttpInterceptor } from './http-interceptor'; 6 | import { PasswordMatcherDirective } from './password-macher'; 7 | 8 | export * from './ax-lib'; 9 | 10 | @NgModule({ 11 | providers: [{ 12 | provide: Http, 13 | useFactory: (backend: XHRBackend, options: RequestOptions) => new HttpInterceptor(backend, options), 14 | deps: [XHRBackend, RequestOptions], 15 | }, { 16 | provide: '', 17 | useValue: `${process.env.API_PROTOCOL}${process.env.API_URI}`, 18 | }], 19 | imports: [ 20 | AxLibModule, 21 | ], 22 | declarations: [ 23 | PasswordMatcherDirective, 24 | ], 25 | exports: [ 26 | PasswordMatcherDirective, 27 | ], 28 | }) 29 | export class AxcCommonModule { 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/app/common/msg-box/index.ts: -------------------------------------------------------------------------------- 1 | export * from './msg-box.module'; 2 | -------------------------------------------------------------------------------- /ui/src/app/common/msg-box/msg-box.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /ui/src/app/common/msg-box/msg-box.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/css/config'; 2 | 3 | .msg-box { 4 | max-width: 460px; 5 | margin: 80px auto; 6 | padding: 50px 50px 80px; 7 | text-align: center; 8 | background-color: $ax-color-bg-light; 9 | box-shadow: 3px 3px 4px rgba(#000, .07); 10 | border-radius: 3px; 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/app/common/msg-box/msg-box.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-msg-box', 5 | templateUrl: './msg-box.component.html', 6 | styles: [require('./msg-box.component.scss')], 7 | }) 8 | export class MsgBoxComponent { 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/common/msg-box/msg-box.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { MsgBoxComponent } from './msg-box.component'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | MsgBoxComponent, 9 | ], 10 | imports: [ 11 | RouterModule, 12 | ], 13 | exports: [ 14 | MsgBoxComponent, 15 | ], 16 | }) 17 | export class MsgBoxModule { 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/common/notifications/index.ts: -------------------------------------------------------------------------------- 1 | export * from './notifications.service'; 2 | export * from './notifications.module'; 3 | -------------------------------------------------------------------------------- /ui/src/app/common/notifications/notification.model.ts: -------------------------------------------------------------------------------- 1 | export enum NotificationTypes { 2 | Success, 3 | Warning, 4 | Error, 5 | } 6 | 7 | export class Notification { 8 | public content: string; 9 | public type: NotificationTypes; 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/app/common/notifications/notifications.component.html: -------------------------------------------------------------------------------- 1 |
2 | 15 |
16 | -------------------------------------------------------------------------------- /ui/src/app/common/notifications/notifications.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/css/config'; 2 | 3 | .ax-notifications-list { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | z-index: $ax-zIndex-notification; 9 | box-shadow: 2px 0 6px rgba(#000, .3); 10 | } 11 | 12 | .ax-notification { 13 | position: absolute; 14 | top: 0; 15 | right: 0; 16 | left: 0; 17 | height: $ax-topbar-height; 18 | padding: 0 60px 0 30px; 19 | line-height: $ax-topbar-height; 20 | font-size: 20px; 21 | color: #fff; 22 | 23 | &.success { 24 | background-color: $ax-color-success; 25 | } 26 | 27 | &.warning { 28 | background-color: $ax-color-warning; 29 | } 30 | 31 | &.error { 32 | background-color: $ax-color-failed; 33 | } 34 | 35 | &__ico { 36 | vertical-align: middle; 37 | margin-right: 10px; 38 | border-radius: 50%; 39 | border: 1px solid rgba(#fff, .4); 40 | } 41 | 42 | &__close { 43 | position: absolute; 44 | top: 50%; 45 | right: 30px; 46 | transform: translateY(-50%); 47 | cursor: pointer; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ui/src/app/common/notifications/notifications.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NotificationsService } from '.'; 3 | import { NotificationTypes, Notification } from './notification.model'; 4 | 5 | @Component({ 6 | selector: 'axc-notifications', 7 | templateUrl: './notifications.component.html', 8 | styles: [require('./notifications.component.scss')], 9 | }) 10 | export class NotificationsComponent { 11 | 12 | public notifications: Notification[]; 13 | public notificationTypes = NotificationTypes; 14 | 15 | constructor(private notificationsService: NotificationsService) { 16 | this.notifications = notificationsService.notifications; 17 | } 18 | 19 | public close(index: number) { 20 | this.notificationsService.close(index); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/app/common/notifications/notifications.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { NotificationsComponent } from './notifications.component'; 5 | import { NotificationsService } from './notifications.service'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | NotificationsComponent, 10 | ], 11 | imports: [ 12 | CommonModule, 13 | ], 14 | exports: [ 15 | NotificationsComponent, 16 | ], 17 | providers: [ 18 | NotificationsService, 19 | ], 20 | }) 21 | export class NotificationsModule { 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/app/common/notifications/notifications.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { NotificationTypes, Notification } from './notification.model'; 4 | 5 | @Injectable() 6 | export class NotificationsService { 7 | 8 | public notifications: Notification[] = []; 9 | 10 | public success(content: string) { 11 | this.createNotification(NotificationTypes.Success, content); 12 | } 13 | 14 | public warning(content: string) { 15 | this.createNotification(NotificationTypes.Warning, content); 16 | } 17 | 18 | public error(content: string) { 19 | this.createNotification(NotificationTypes.Error, content); 20 | } 21 | 22 | public close(index: number) { 23 | delete this.notifications[index]; 24 | } 25 | 26 | private createNotification(type: NotificationTypes, content: string) { 27 | let newNotificationIndex = this.notifications.push({ content, type }) - 1; 28 | 29 | // Autohide 30 | setTimeout(() => { 31 | delete this.notifications[newNotificationIndex]; 32 | }, 5000); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui/src/app/common/password-macher.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | import { AbstractControl, NG_VALIDATORS } from '@angular/forms'; 3 | 4 | function passwordMatcher(c: AbstractControl) { 5 | if (!c.get('password') || !c.get('confirm')) { 6 | return null; 7 | } 8 | return c.get('password').value === c.get('confirm').value 9 | ? null : {noMatch: true}; 10 | 11 | } 12 | 13 | @Directive({ 14 | selector: '[axc-password-matcher]', 15 | providers: [ 16 | {provide: NG_VALIDATORS, multi: true, useValue: passwordMatcher}, 17 | ], 18 | }) 19 | export class PasswordMatcherDirective { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/common/shared/constants.ts: -------------------------------------------------------------------------------- 1 | export const FIELD_PATTERNS = { 2 | CIDR: '(^$|\\d{1,3}(\\.\\d{1,3}){3}\\/\\d{1,2}$)', 3 | ENV_NAME: '^[A-Za-z0-9]([-A-Za-z0-9_]*)?[A-Za-z0-9]$', 4 | // Password will match only: 8+ letters, at least 1 lower case letter, 5 | // at least 1 upper case letter, and at least 1 special character 6 | PASSWORD: '^(?=.*?[A-Za-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$', 7 | // Email regex 8 | EMAIL: '^[a-z0-9]+(\.[_a-z0-9]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,15})$', 9 | }; 10 | 11 | export const ERROR_CODES = { 12 | INTERNAL_ERROR: 'INTERNAL_ERROR', 13 | INVALID_REQUEST: 'INVALID_REQUEST', 14 | UNAUTHENTICATED_REQUEST: 'UNAUTHENTICATED_REQUEST', 15 | UNAUTHORIZED_REQUEST: 'UNAUTHORIZED_REQUEST', 16 | ENTITY_NOT_FOUND: 'ENTITY_NOT_FOUND', 17 | }; 18 | 19 | export interface AppError { 20 | code?: string; 21 | 22 | message?: string; 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/app/common/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | -------------------------------------------------------------------------------- /ui/src/app/common/validate-onblur.ts: -------------------------------------------------------------------------------- 1 | import { Directive, HostListener } from '@angular/core'; 2 | import { NgControl } from '@angular/forms'; 3 | 4 | @Directive({ 5 | selector: '[axc-validate-onblur]', 6 | }) 7 | export class ValidateOnBlurDirective { 8 | constructor(public formControl: NgControl) { 9 | } 10 | 11 | @HostListener('focus') 12 | public onFocus() { 13 | this.formControl.control.markAsUntouched(false); 14 | } 15 | 16 | @HostListener('blur') 17 | public onBlur() { 18 | this.formControl.control.markAsTouched(true); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/config/aws-config.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Create a report to get started

5 |
6 |
7 |

Report Settings

8 |

You should have at least one bucket configured to see data

9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | 17 | 19 |
20 |
21 | Retention Days is required. 22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 |
33 |
34 |

Manage Buckets

35 |

Enter one or more S3 buckets containing your 36 | AWS Cost & Usage Reports, along with access credentials to the bucket. 38 | 40 | 41 |

42 |
43 |
44 | 46 |
47 |
48 | 49 |
50 | -------------------------------------------------------------------------------- /ui/src/app/config/aws-config.component.scss: -------------------------------------------------------------------------------- 1 | .aws-config { 2 | .ax-form-row { 3 | padding: 0em 0.5em; 4 | } 5 | .action-item { 6 | margin:1em 0; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ui/src/app/config/aws-config.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | 4 | import { ConfigService } from '../services'; 5 | import { NotificationsService } from '../common/notifications/notifications.service'; 6 | import { FIELD_PATTERNS } from '../common/shared'; 7 | import { Bucket, Report } from '../model'; 8 | 9 | const DEFAULT_LOGIN_ERROR = 'Unable to login. Please try again later.'; 10 | 11 | @Component({ 12 | selector: 'axc-aws-config', 13 | templateUrl: './aws-config.component.html', 14 | styles: [ 15 | require('./aws-config.component.scss'), 16 | ], 17 | }) 18 | export class AWSConfigComponent implements OnInit { 19 | 20 | public config: Report; 21 | public buckets: Bucket[]; 22 | public report_name: string = 'default'; 23 | public retention_days: number = 365; 24 | 25 | constructor( 26 | private router: Router, 27 | private activatedRoute: ActivatedRoute, 28 | private notificationsService: NotificationsService, 29 | private configService: ConfigService) { 30 | } 31 | 32 | public async ngOnInit() { 33 | let data: Report = await this.configService.getReportConfig(); 34 | if (data) { 35 | this.updateConfig(data); 36 | } 37 | } 38 | 39 | public updateConfig(data: Report) { 40 | this.config = data; 41 | this.report_name = this.config.report_name; 42 | this.retention_days = this.config.retention_days; 43 | this.buckets = this.config.buckets; 44 | if (this.buckets.length === 0) { 45 | this.addBucket(); 46 | } 47 | } 48 | 49 | public async saveReportObj() { 50 | try { 51 | let report: Report; 52 | if (!this.config) { 53 | report = await this.configService.createReportConfig({ 54 | report_name: this.report_name, 55 | retention_days: this.retention_days, 56 | }); 57 | } else { 58 | report = await this.configService.updateReportConfig(this.config.id, { 59 | report_name: this.report_name, 60 | retention_days: this.retention_days, 61 | }); 62 | } 63 | 64 | this.updateConfig(report); 65 | } catch (e) { 66 | this.notificationsService.error(e.message); 67 | } 68 | } 69 | 70 | public onBucketDelete(data) { 71 | this.buckets.splice(data.identifier, 1); 72 | if (this.buckets.length === 0) { 73 | this.addBucket(); 74 | } 75 | } 76 | 77 | public onBucketSave(data: { bucket: Bucket, identifier: any }) { 78 | this.buckets[data.identifier] = data.bucket; 79 | } 80 | public addBucket() { 81 | this.buckets.unshift(new Bucket()); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /ui/src/app/config/bucket.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 | 7 |
8 |
9 | Bucket name is required. 10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 | Report path is required. 19 |
20 |
21 |
22 | 23 |
24 | 25 | 26 |
27 |
28 | AWS Key ID is required. 29 |
30 |
31 |
32 | 33 |
34 | 35 | 36 |
37 |
38 | AWS Secret Access Key is required. 39 |
40 |
41 |
42 | 43 |
44 | 45 | 46 | 47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /ui/src/app/config/bucket.component.scss: -------------------------------------------------------------------------------- 1 | .bucket-config { 2 | margin:1em 0; 3 | .ax-form-row { 4 | padding: 0em 0.5em; 5 | } 6 | .action-item { 7 | margin:1em 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/config/config.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /ui/src/app/config/config.component.scss: -------------------------------------------------------------------------------- 1 | @import './../../assets/css/config'; 2 | 3 | .aws-config { 4 | width:80%; 5 | margin:0px auto; 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/app/config/config.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-config', 5 | templateUrl: './config.component.html', 6 | styles: [ 7 | require('./config.component.scss'), 8 | ], 9 | }) 10 | export class ConfigComponent { 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/app/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config.component'; 2 | export * from './aws-config.component'; 3 | export * from './bucket.component'; 4 | -------------------------------------------------------------------------------- /ui/src/app/config/keys-config/keys-config.component.html: -------------------------------------------------------------------------------- 1 |

Manage Private Key & Public Certificates

2 | 3 |
4 |
5 | 6 | 8 |
9 |
10 | 11 | 13 |
14 |
15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /ui/src/app/config/keys-config/keys-config.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/app/config/keys-config/keys-config.component.scss -------------------------------------------------------------------------------- /ui/src/app/config/keys-config/keys-config.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ConfigService } from '../../services/config.service'; 3 | import { Config } from '../../model/config.model'; 4 | import { NotificationsService } from '../../common/notifications/notifications.service'; 5 | 6 | @Component({ 7 | selector: 'axc-keys-config', 8 | templateUrl: './keys-config.component.html', 9 | styles: [ 10 | require('./keys-config.component.scss'), 11 | ], 12 | }) 13 | export class KeysConfigComponent implements OnInit { 14 | 15 | public config: Config = new Config(); 16 | 17 | constructor(private configService: ConfigService, private notificationsService: NotificationsService) { 18 | } 19 | 20 | public ngOnInit() { 21 | this.configService.getConfig().then((data: Config) => this.config = data); 22 | } 23 | 24 | public update() { 25 | this.configService.updateConfig(this.config).then((data: Config) => { 26 | this.config = data; 27 | this.notificationsService.success('Config was updated.'); 28 | }, () => { 29 | this.notificationsService.error('Something went wrong.'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/app/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |

Dashboard Foo

2 | -------------------------------------------------------------------------------- /ui/src/app/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-dashboard', 5 | templateUrl: './dashboard.component.html', 6 | }) 7 | export class DashboardComponent { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dashboard.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/environment.ts: -------------------------------------------------------------------------------- 1 | import { enableDebugTools, disableDebugTools } from '@angular/platform-browser'; 2 | import { enableProdMode, ApplicationRef } from '@angular/core'; 3 | 4 | let PROVIDERS: any[] = [ 5 | // common env directives 6 | ]; 7 | 8 | // Angular debug tools in the dev console 9 | // https://github.com/angular/angular/blob/86405345b781a9dc2438c0fbe3e9409245647019/TOOLS_JS.md 10 | 11 | /* tslint:disable */ 12 | let decorateModuleRefInternal = function identity(value: T): T { return value; }; 13 | /* tslint:enable */ 14 | 15 | if ('production' === ENV) { 16 | // Production 17 | disableDebugTools(); 18 | enableProdMode(); 19 | 20 | PROVIDERS = [ 21 | ...PROVIDERS, 22 | ]; 23 | 24 | } else { 25 | decorateModuleRefInternal = (modRef: any) => { 26 | const appRef = modRef.injector.get(ApplicationRef); 27 | const cmpRef = appRef.components[0]; 28 | 29 | let ng = ( window).ng; 30 | enableDebugTools(cmpRef); 31 | ( window).ng.probe = ng.probe; 32 | ( window).ng.coreTokens = ng.coreTokens; 33 | return modRef; 34 | }; 35 | 36 | // Development 37 | PROVIDERS = [ 38 | ...PROVIDERS, 39 | ]; 40 | 41 | } 42 | 43 | export const decorateModuleRef = decorateModuleRefInternal; 44 | 45 | export const ENV_PROVIDERS = [ 46 | ...PROVIDERS, 47 | ]; 48 | -------------------------------------------------------------------------------- /ui/src/app/error-page/error-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'axc-error-page', 6 | templateUrl: './error-page.html', 7 | }) 8 | export class ErrorPageComponent implements OnInit { 9 | private msg: string = ''; 10 | private errorCode: string = ''; 11 | 12 | private errorType: string = 'System Error'; 13 | constructor( 14 | private activatedRoute: ActivatedRoute) { 15 | } 16 | 17 | public ngOnInit() { 18 | this.activatedRoute.params.subscribe((params) => { 19 | if (params && params['code']) { 20 | this.errorCode = params['code']; 21 | } 22 | if (params && params['msg']) { 23 | this.msg = decodeURIComponent(params['msg']); 24 | } 25 | if (params && params['type']) { 26 | this.errorType = params['type']; 27 | } 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/app/error-page/error-page.html: -------------------------------------------------------------------------------- 1 |
2 |

{{errorCode}}

3 |

Error Type - {{errorType}}

4 |

{{msg}}

5 |
6 | -------------------------------------------------------------------------------- /ui/src/app/error-page/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error-page.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/eula/eula.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | ALL PRODUCTS ARE PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND FROM ANYONE, INCLUDING WITHOUT LIMITATION, ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE OR NONINFRINGEMENT. FURTHER, APPLATIX DOES NOT WARRANT RESULTS OF USE OR THAT THE PRODUCTS ARE BUG FREE OR THAT THE PRODUCT’S USE WILL BE UNINTERRUPTED. 4 |

5 | 6 | 7 |

You have already accepted the terms.

8 | 9 |
10 | -------------------------------------------------------------------------------- /ui/src/app/eula/eula.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | 4 | import { Config } from '../model/config.model'; 5 | import { ConfigService, AuthenticationService } from '../services'; 6 | import { NotificationsService } from '../common/notifications/notifications.service'; 7 | 8 | @Component({ 9 | selector: 'axc-eula', 10 | templateUrl: './eula.component.html', 11 | styles: [ 12 | require('./eula.scss'), 13 | ], 14 | }) 15 | export class EulaComponent implements OnInit { 16 | public config: Config; 17 | public fwdUrl: string = '/app/report'; 18 | 19 | constructor( 20 | private router: Router, 21 | private activatedRoute: ActivatedRoute, 22 | private notificationsService: NotificationsService, 23 | private configService: ConfigService, 24 | private authenticationService: AuthenticationService) { 25 | } 26 | 27 | public async ngOnInit() { 28 | this.config = await this.configService.getConfig(); 29 | this.activatedRoute.params.subscribe((params) => { 30 | this.fwdUrl = params['fwd'] ? params['fwd'] : this.fwdUrl; 31 | }); 32 | } 33 | 34 | public async onAccept() { 35 | let c: Config = new Config(); 36 | c.eula_accepted = true; 37 | await this.configService.updateConfig(c); 38 | this.router.navigateByUrl(this.fwdUrl); 39 | } 40 | 41 | public async onDeny() { 42 | await this.authenticationService.doLogout(); 43 | this.router.navigateByUrl('base/login'); 44 | } 45 | 46 | public onAlreadyAccepted() { 47 | this.router.navigateByUrl(this.fwdUrl); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ui/src/app/eula/eula.scss: -------------------------------------------------------------------------------- 1 | 2 | .eula { 3 | p { 4 | font-size: 1em; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/app/eula/index.ts: -------------------------------------------------------------------------------- 1 | export * from './eula.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.module'; 2 | -------------------------------------------------------------------------------- /ui/src/app/layout/base/base-layout.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /ui/src/app/layout/base/base-layout.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/css/config'; 2 | 3 | .ax-layout { 4 | padding: #{$ax-topbar-height} 0 0 0; 5 | 6 | &__main { 7 | padding: 20px; 8 | margin: 0px auto; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ui/src/app/layout/base/base-layout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, OnInit } from '@angular/core'; 2 | import { Router, RouterOutlet, ActivatedRouteSnapshot, NavigationEnd } from '@angular/router'; 3 | 4 | export interface LayoutSettings { 5 | 6 | } 7 | 8 | export interface HasLayoutSettings { 9 | layoutSettings: LayoutSettings; 10 | } 11 | 12 | @Component({ 13 | selector: 'axc-base-layout', 14 | templateUrl: './base-layout.component.html', 15 | styles: [ 16 | require('./base-layout.component.scss'), 17 | ], 18 | }) 19 | export class BaseLayoutComponent implements OnInit { 20 | 21 | public title: string; 22 | public layoutSettings: LayoutSettings; 23 | @ViewChild(RouterOutlet) 24 | public routerOutlet: RouterOutlet; 25 | 26 | constructor(private router: Router) { } 27 | 28 | public ngOnInit() { 29 | this.router.events.subscribe((event) => { 30 | if (event instanceof NavigationEnd) { 31 | this.title = this.getDeepestTitle(this.router.routerState.snapshot.root); 32 | if (this.routerOutlet.isActivated) { 33 | let component: any = this.routerOutlet.component; 34 | this.layoutSettings = component ? component.layoutSettings || {} : {}; 35 | } else { 36 | this.layoutSettings = null; 37 | } 38 | } 39 | }); 40 | } 41 | 42 | private getDeepestTitle(routeSnapshot: ActivatedRouteSnapshot) { 43 | let title = routeSnapshot.data ? routeSnapshot.data['title'] : ''; 44 | 45 | if (routeSnapshot.firstChild) { 46 | title = this.getDeepestTitle(routeSnapshot.firstChild) || title; 47 | } 48 | 49 | return title; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ui/src/app/layout/base/base-layout.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | import { AxLibModule, AxcCommonModule } from '../../common'; 6 | import { BaseLayoutComponent } from '.'; 7 | import { TopbarComponent } from './topbar'; 8 | import { NotificationsModule } from '../../common/notifications'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | BaseLayoutComponent, 13 | TopbarComponent, 14 | ], 15 | imports: [ 16 | CommonModule, 17 | RouterModule, 18 | NotificationsModule, 19 | AxLibModule, 20 | AxcCommonModule, 21 | ], 22 | exports: [ 23 | TopbarComponent, 24 | AxcCommonModule, 25 | ], 26 | }) 27 | export class BaseLayoutModule {} 28 | -------------------------------------------------------------------------------- /ui/src/app/layout/base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base-layout.component'; 2 | export * from './base-layout.module'; 3 | export * from './topbar'; 4 | -------------------------------------------------------------------------------- /ui/src/app/layout/base/topbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './topbar.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/layout/base/topbar/topbar.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 8 |
9 |
10 |
11 | Spend Analytics 12 |
13 |
14 | Setup 15 | Help 16 | Logout 17 |
18 |
19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /ui/src/app/layout/base/topbar/topbar.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../assets/css/config'; 2 | 3 | .ax-topbar { 4 | position: fixed; 5 | top: 0; 6 | z-index: $ax-zIndex-topbar; 7 | width: 100%; 8 | height: $ax-topbar-height; 9 | line-height: $ax-topbar-height; 10 | background-color: $ax-color-base-dark; 11 | 12 | &__logo { 13 | position: relative; 14 | color: #fff; 15 | font-size: 1.5em; 16 | 17 | &::after { 18 | position: absolute; 19 | top: 10px; 20 | right: 0; 21 | bottom: 10px; 22 | width: 1px; 23 | background-color: rgba($ax-color-gray-5, .5); 24 | content: ''; 25 | } 26 | } 27 | 28 | &__menu { 29 | font-size: 1.2em; 30 | padding-right: 10px; 31 | 32 | a { 33 | color: $ax-color-gray-2; 34 | } 35 | } 36 | 37 | &__menu-right { 38 | font-size: .8em; 39 | 40 | a { 41 | position: relative; 42 | color: $ax-color-gray-2; 43 | 44 | &:not(:last-child) { 45 | margin-right: 20px; 46 | 47 | // &::after { 48 | // position: absolute; 49 | // top: -2px; 50 | // right: -10px; 51 | // bottom: -2px; 52 | // width: 1px; 53 | // background-color: rgba($ax-color-gray-5, .5); 54 | // content: ''; 55 | // } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ui/src/app/layout/base/topbar/topbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | 4 | import * as layout from '../../../layout'; 5 | import { AuthenticationService, UserService } from '../../../services'; 6 | 7 | @Component({ 8 | selector: 'axc-topbar', 9 | templateUrl: './topbar.component.html', 10 | styles: [ 11 | require('./topbar.component.scss'), 12 | ], 13 | }) 14 | export class TopbarComponent { 15 | 16 | @Input() 17 | public settings: layout.LayoutSettings; 18 | 19 | @Input() 20 | public title: string; 21 | 22 | constructor( 23 | private router: Router, 24 | private activatedRoute: ActivatedRoute, 25 | private authenticationService: AuthenticationService, 26 | private userService: UserService) { 27 | } 28 | 29 | public async doLogout() { 30 | await this.authenticationService.doLogout(); 31 | } 32 | public goToReport() { 33 | window.location.href = 'app/report'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/app/layout/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | export * from './intro'; 3 | -------------------------------------------------------------------------------- /ui/src/app/layout/intro/index.ts: -------------------------------------------------------------------------------- 1 | export * from './intro-layout.component'; 2 | export * from './intro-layout.module'; 3 | -------------------------------------------------------------------------------- /ui/src/app/layout/intro/intro-layout.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /ui/src/app/layout/intro/intro-layout.component.scss: -------------------------------------------------------------------------------- 1 | @import './../../../assets/css/config'; 2 | 3 | .ax-layout { 4 | height: calc(100%); 5 | padding-top: $ax-topbar-height; 6 | 7 | &__main { 8 | max-width: 500px; 9 | margin: 100px auto; 10 | padding: 40px; 11 | background-color: $ax-color-bg-light; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/app/layout/intro/intro-layout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-intro-layout', 5 | templateUrl: './intro-layout.component.html', 6 | styles: [ 7 | require('./intro-layout.component.scss'), 8 | ], 9 | }) 10 | export class IntroLayoutComponent { 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/app/layout/intro/intro-layout.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | import { IntroLayoutComponent } from '.'; 5 | import { NotificationsModule } from '../../common/notifications'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | IntroLayoutComponent, 10 | ], 11 | imports: [ 12 | RouterModule, 13 | NotificationsModule, 14 | ], 15 | }) 16 | export class IntroLayoutModule {} 17 | -------------------------------------------------------------------------------- /ui/src/app/login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 |
7 | 8 | 9 |
10 |
11 | Username is required 12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 | Password is required 21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /ui/src/app/login/login.component.scss: -------------------------------------------------------------------------------- 1 | @import './../../assets/css/config'; 2 | 3 | .login { 4 | &__action-link { 5 | text-transform: uppercase; 6 | font-size: 0.813em; 7 | } 8 | .logo { 9 | text-align: center; 10 | margin-bottom: 25px; 11 | 12 | img { 13 | height: 40px; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | 4 | import { AuthenticationService, UserService } from '../services'; 5 | import { NotificationsService } from '../common/notifications/notifications.service'; 6 | import { FIELD_PATTERNS } from '../common/shared'; 7 | 8 | const DEFAULT_LOGIN_ERROR = 'Unable to login. Please try again later.'; 9 | 10 | @Component({ 11 | selector: 'axc-login', 12 | templateUrl: './login.component.html', 13 | styles: [ 14 | require('./login.component.scss'), 15 | ], 16 | }) 17 | export class LoginComponent implements OnInit { 18 | public username: string; 19 | public password: string; 20 | public newPassword: string; 21 | public confirmPassword: string; 22 | public token: string; 23 | public fwdUrl: string = '/app/report'; 24 | 25 | public passwordRegex: string = FIELD_PATTERNS.PASSWORD; 26 | 27 | constructor( 28 | private router: Router, 29 | private activatedRoute: ActivatedRoute, 30 | private authenticationService: AuthenticationService, 31 | private notificationsService: NotificationsService, 32 | private userService: UserService) { 33 | } 34 | 35 | public ngOnInit() { 36 | // Read the url for any forward urls 37 | this.activatedRoute.params.subscribe((params) => { 38 | this.fwdUrl = params['fwd'] ? params['fwd'] : this.fwdUrl; 39 | }); 40 | } 41 | 42 | public async doLogin() { 43 | try { 44 | let success: any = await this.authenticationService.doLogin(this.username, this.password); 45 | if (success.config && success.config.eula_accepted) { 46 | this.router.navigateByUrl(this.fwdUrl); 47 | } else { 48 | this.router.navigate(['/base/eula', {fwdUrl: this.fwdUrl}]); 49 | } 50 | 51 | } catch (err) { 52 | this.notificationsService.error(err.message || DEFAULT_LOGIN_ERROR); 53 | } 54 | } 55 | 56 | public get loginUrl(): string { 57 | let url = '/base/login'; 58 | if (this.fwdUrl && this.fwdUrl !== '') { 59 | url += '/' + encodeURIComponent(this.fwdUrl); 60 | } 61 | return url; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ui/src/app/model/account.model.ts: -------------------------------------------------------------------------------- 1 | export interface Account { 2 | aws_account_id: string; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/app/model/bucket.model.ts: -------------------------------------------------------------------------------- 1 | export class Bucket { 2 | public id?: string; 3 | public bucketname: string; 4 | public report_path: string; 5 | public aws_access_key_id: string; 6 | public aws_secret_access_key: string; 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/app/model/config.model.ts: -------------------------------------------------------------------------------- 1 | export class Config { 2 | public private_key: string; 3 | public public_certificate: string; 4 | public eula_accepted: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/app/model/graph.model.ts: -------------------------------------------------------------------------------- 1 | export class Graph { 2 | public columns: string[] = []; 3 | public tags: Map = new Map(); 4 | public name: string = ''; 5 | public values: Array = Array(); 6 | constructor(data) { 7 | if (typeof data === 'object') { 8 | for (let key in data) { 9 | if (data.hasOwnProperty(key)) { 10 | 11 | this[key] = data[key]; 12 | } 13 | } 14 | } 15 | } 16 | 17 | public getSeriesDisplayName(): string { 18 | let displayName = this.tags['display_name']; 19 | if (displayName === '') { 20 | return 'Misc. Charges'; 21 | } else { 22 | return displayName; 23 | } 24 | } 25 | 26 | public getSumOfAllValues(): number { 27 | let sum = 0; 28 | 29 | for (let i = 0; i < this.values.length; i++) { 30 | let v = this.values[i]; 31 | if (v.length === 2) { 32 | sum = sum + v[1]; 33 | } 34 | } 35 | return sum; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/app/model/index.ts: -------------------------------------------------------------------------------- 1 | export { User } from './user.model'; 2 | export { Dimension } from './dimension.model'; 3 | export { Graph } from './graph.model'; 4 | export { Bucket } from './bucket.model'; 5 | export { Account } from './account.model'; 6 | export { Report } from './report.model'; 7 | export { UsageUnit } from './usage.model'; 8 | export { Config } from './config.model'; 9 | -------------------------------------------------------------------------------- /ui/src/app/model/report.model.ts: -------------------------------------------------------------------------------- 1 | import { Bucket } from './bucket.model'; 2 | import { Account } from './account.model'; 3 | 4 | export interface Report { 5 | id?: string; 6 | ctime?: string; 7 | owner_user_id?: string; 8 | report_name?: string; 9 | aws_secret_access_key?: string; 10 | retention_days?: number; 11 | buckets?: Bucket[]; 12 | accounts?: Account[]; 13 | status?: string; 14 | status_detail?: string; 15 | } 16 | -------------------------------------------------------------------------------- /ui/src/app/model/usage.model.ts: -------------------------------------------------------------------------------- 1 | export interface UsageUnit { 2 | name: string; 3 | usagefamilies: string[]; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/app/model/user.model.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | public email: string; 3 | 4 | constructor(data?) { 5 | if (typeof data === 'object') { 6 | for (let key in data) { 7 | if (data.hasOwnProperty(key)) { 8 | this[key] = data[key]; 9 | } 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/app/no-content/index.ts: -------------------------------------------------------------------------------- 1 | export * from './no-content.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/no-content/no-content.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-no-content', 5 | template: ` 6 |
7 |

404: page missing

8 |
9 | `, 10 | }) 11 | export class NoContentComponent { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/app/password/index.ts: -------------------------------------------------------------------------------- 1 | export * from './password.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/password/password-data.ts: -------------------------------------------------------------------------------- 1 | export class PasswordData { 2 | public currentPassword: string; 3 | public newPassword: string; 4 | public confirmPassword: string; 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/app/password/password.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 6 |
7 |
8 | Current Password is required 9 |
10 |
11 |
12 |
13 | 14 | 16 |
17 |
18 | New Password is required 19 |
20 |
21 | Password should contain at least 8 characters. 22 |
23 |
24 |
25 |
26 | 27 | 29 |
30 |
31 | Password confirmation is required 32 |
33 |
34 |
35 | Your password and confirmation password do not match 36 |
37 |
38 |
39 | 40 |
41 |
42 | -------------------------------------------------------------------------------- /ui/src/app/password/password.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { UserService } from '../services'; 4 | import { AppError } from '../common/shared'; 5 | import { NotificationsService } from '../common/notifications'; 6 | import { PasswordData } from './password-data'; 7 | 8 | @Component({ 9 | selector: 'axc-password', 10 | templateUrl: './password.component.html', 11 | }) 12 | export class PasswordComponent { 13 | 14 | public changePasswordData: PasswordData; 15 | 16 | constructor(private userService: UserService, private notificationService: NotificationsService) { 17 | this.changePasswordData = new PasswordData(); 18 | } 19 | 20 | public async doChangePassword(form) { 21 | await this.userService.changePassword(this.changePasswordData).then((res) => { 22 | this.notificationService.success('Password has been updated'); 23 | this.changePasswordData = new PasswordData(); 24 | form.reset(); 25 | }).catch((err) => { 26 | let appErr = err; 27 | this.notificationService.error(appErr.message || 'Can not update password. Please try again later.'); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/app/pipe/axc-pipe.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { AxcDatePipe } from './date.pipe'; 3 | import { DatePipe } from '@angular/common'; 4 | import { TrimTextPipe } from './trim-text.pipe'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | AxcDatePipe, 9 | TrimTextPipe, 10 | ], 11 | exports: [ 12 | AxcDatePipe, 13 | TrimTextPipe, 14 | ], 15 | providers: [ 16 | DatePipe, 17 | ], 18 | }) 19 | export class AxcPipeModule { 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/pipe/date.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { DatePipe } from '@angular/common'; 3 | 4 | @Pipe({ 5 | name: 'axcDate', 6 | }) 7 | 8 | export class AxcDatePipe implements PipeTransform { 9 | private dateFormat: string = 'yyyy/MM/dd HH:mm'; 10 | 11 | constructor (private datePipe: DatePipe) { 12 | } 13 | 14 | public transform(value: string, pattern: string) { 15 | return this.datePipe.transform(value, pattern ? pattern : this.dateFormat); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/app/pipe/index.ts: -------------------------------------------------------------------------------- 1 | export { TrimTextPipe } from './trim-text.pipe'; 2 | export { AxcDatePipe } from './date.pipe'; 3 | -------------------------------------------------------------------------------- /ui/src/app/pipe/trim-text.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'trimText', 5 | }) 6 | 7 | export class TrimTextPipe implements PipeTransform { 8 | public transform(value: string, letters = 30) { 9 | let maxLength = letters; 10 | let ret = value; 11 | if (ret.length > maxLength) { 12 | ret = ret.substr(0, maxLength - 3) + '…'; 13 | } 14 | return ret; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ui/src/app/report/advanced-filter.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{filter.display_name}}

3 |
    4 |
  • All {{filter.display_name}}
  • 5 |
  • {{option.display_name}}
  • 6 |
7 |
8 | -------------------------------------------------------------------------------- /ui/src/app/report/advanced-filter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, EventEmitter, Output } from '@angular/core'; 2 | import { Dimension } from '../model'; 3 | 4 | @Component({ 5 | selector: 'axc-advanced-filter', 6 | templateUrl: './advanced-filter.component.html', 7 | styles: [ 8 | require('./advanced-filter.scss'), 9 | ], 10 | }) 11 | export class AdvancedFilterComponent implements OnInit { 12 | @Input() 13 | public filter: Dimension; 14 | 15 | @Output() 16 | public onChange: EventEmitter = new EventEmitter(); 17 | 18 | public selectedFilters: Map = new Map(); 19 | 20 | public ngOnInit() { 21 | // do nothing 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/app/report/advanced-filter.scss: -------------------------------------------------------------------------------- 1 | .sub-filters { 2 | float:left; 3 | width:33%; 4 | padding:15px; 5 | ul { 6 | margin: 0px; 7 | width: 100%; 8 | 9 | li { 10 | margin:10px 0px; 11 | border-radius: 5px; 12 | padding: 0px; 13 | list-style-type: none; 14 | border: 1px solid #C2D9F2; 15 | line-height: 2em; 16 | font-size: 1em; 17 | padding-left: 1em; 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | white-space: nowrap; 21 | cursor: pointer; 22 | } 23 | .selectedTab { 24 | background: #D8E7F5; 25 | } 26 | .hide { 27 | display: none; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/app/report/config.ts: -------------------------------------------------------------------------------- 1 | export interface GroupBy { 2 | display_name: string; 3 | key: string; 4 | } 5 | 6 | export interface Interval { 7 | display_name: string; 8 | key: string; 9 | } 10 | 11 | export const resourceMetricKey = 'Resource Count'; 12 | export const DefaultMetrics = ['Unblended Cost', 'Blended Cost', resourceMetricKey]; 13 | 14 | export const groupByDefaults: GroupBy[] = [ 15 | { 16 | display_name: 'None', 17 | key: '', 18 | }, 19 | { 20 | display_name: 'Account', 21 | key: 'accounts', 22 | }, 23 | { 24 | display_name: 'Region', 25 | key: 'regions', 26 | }, 27 | { 28 | display_name: 'Services', 29 | key: 'services', 30 | }, 31 | ]; 32 | 33 | export const intervalDefaults: Interval[] = [ 34 | { 35 | display_name: 'Hrs', 36 | key: '1h', 37 | }, 38 | { 39 | display_name: 'Day', 40 | key: '1d', 41 | }, 42 | { 43 | display_name: 'Week', 44 | key: '1w', 45 | }, 46 | { 47 | display_name: 'Month', 48 | key: '1M', 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /ui/src/app/report/data-chart/data-chart.component.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /ui/src/app/report/data-chart/data-chart.scss: -------------------------------------------------------------------------------- 1 | text { 2 | font-size:12px; 3 | } 4 | svg { 5 | display: block; 6 | float: left; 7 | } 8 | 9 | .nvd3.nv-pie.nv-chart-donut1 .nv-pie-title { 10 | opacity: 0.4; 11 | fill: rgba(224, 116, 76, 0.91); 12 | } 13 | 14 | #reportDonut { 15 | height: 550px !important; 16 | width: 100% !important; 17 | svg { 18 | .c3-line { 19 | stroke-width: 2px; 20 | } 21 | .c3-circle { 22 | stroke-width: 0px; 23 | visibility: hidden; 24 | } 25 | .c3-circle._expanded_ { 26 | stroke-width: 1px; 27 | visibility: visible; 28 | } 29 | } 30 | .c3-tooltip-container { 31 | pointer-events: auto !important; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/app/report/data-chart/index.ts: -------------------------------------------------------------------------------- 1 | export { DataChartComponent } from './data-chart.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/report/index.ts: -------------------------------------------------------------------------------- 1 | export * from './report.component'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { DataChartComponent } from './data-chart'; 5 | import { AdvancedFilterComponent } from './advanced-filter.component'; 6 | import { TagsFilterComponent } from './tags-filter/tags-filter.component'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | DataChartComponent, 11 | AdvancedFilterComponent, 12 | TagsFilterComponent, 13 | ], 14 | exports: [ 15 | DataChartComponent, 16 | AdvancedFilterComponent, 17 | TagsFilterComponent, 18 | ], 19 | imports: [ 20 | CommonModule, 21 | ], 22 | }) 23 | 24 | export class ReportGraphModule { 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/report/tags-filter/tags-filter.component.html: -------------------------------------------------------------------------------- 1 | 9 |
10 |
{{ typeName }}: {{ selectedTags.size }}
11 |
12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /ui/src/app/report/tags-filter/tags-filter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-tags-filter', 5 | templateUrl: './tags-filter.component.html', 6 | }) 7 | export class TagsFilterComponent { 8 | 9 | @Input() 10 | public tags: any[]; 11 | 12 | @Input() 13 | public selectedTags: any; 14 | 15 | @Input() 16 | public typeName: string; 17 | 18 | @Output() 19 | public onRemoveTag: EventEmitter = new EventEmitter(); 20 | 21 | @Output() 22 | public onRefresh: EventEmitter = new EventEmitter(); 23 | 24 | close(tag) { 25 | this.onRemoveTag.next(tag); 26 | this.onRefresh.next({}); 27 | } 28 | 29 | closeAll() { 30 | this.tags.forEach((tag: any) => { 31 | if (this.selectedTags.has(tag.name)) { 32 | this.onRemoveTag.next(tag); 33 | } 34 | }); 35 | this.onRefresh.next({}); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ui/src/app/resolvers/has-no-session.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | 4 | import { AuthenticationService } from '../services/authentication.service'; 5 | import { User } from '../model/user.model'; 6 | 7 | @Injectable() 8 | export class HasNoSessionResolver implements Resolve { 9 | constructor(private authenticationService: AuthenticationService) { } 10 | 11 | public resolve( 12 | route: ActivatedRouteSnapshot, 13 | state: RouterStateSnapshot 14 | ): Promise { 15 | return this.authenticationService.hasNoSession(route, state); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/app/resolvers/report.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | 4 | import { ConfigService } from '../services/config.service'; 5 | import { User } from '../model/user.model'; 6 | 7 | /** 8 | * This resolves that the report configured has at least one bucket 9 | */ 10 | @Injectable() 11 | export class ReportResolver implements Resolve { 12 | constructor(private configService: ConfigService) { } 13 | 14 | public async resolve( 15 | route: ActivatedRouteSnapshot, 16 | state: RouterStateSnapshot, 17 | ): Promise { 18 | return await this.configService.getReportConfig(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/resolvers/who-am-i.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | 4 | import { AuthenticationService } from '../services/authentication.service'; 5 | import { User } from '../model/user.model'; 6 | 7 | /** 8 | * This resolves the WhoAmI information after reload 9 | */ 10 | @Injectable() 11 | export class WhoAmIResolver implements Resolve { 12 | constructor(private authenticationService: AuthenticationService) { } 13 | 14 | public resolve( 15 | route: ActivatedRouteSnapshot, 16 | state: RouterStateSnapshot 17 | ): Promise { 18 | return this.authenticationService.hasSession(route, state); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/services/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http } from '@angular/http'; 3 | import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 4 | import { ConfigService } from './config.service'; 5 | import { User, Config } from '../model'; 6 | 7 | @Injectable() 8 | export class AuthenticationService { 9 | public currentUser: User = new User({}); 10 | private fwdUrl: string = 'app/report'; 11 | 12 | constructor(private router: Router, private http: Http, private configService: ConfigService) { 13 | } 14 | 15 | public getUser(): User { 16 | return this.currentUser || null; 17 | } 18 | 19 | public async doLogin(username: string, password: string): Promise<{ user: User, config: Config }> { 20 | let data = await this.http.post('/auth/login', { username, password }).map((res) => new User(res.json())).toPromise(); 21 | this.currentUser = new User(data); 22 | let configData = await this.configService.getConfig(); 23 | return { user: this.currentUser, config: configData }; 24 | } 25 | 26 | public async doLogout(): Promise { 27 | await this.http.post('/auth/logout', {}).toPromise(); 28 | this.router.navigate(['/base/login']); 29 | this.currentUser = null; 30 | return { success: true }; 31 | } 32 | 33 | public async whoAmI(): Promise { 34 | let user = await this.http.get('/auth/identity').map((res) => new User(res.json())).toPromise(); 35 | this.currentUser = new User(user); 36 | return user; 37 | } 38 | 39 | public async hasSession( 40 | route: ActivatedRouteSnapshot, 41 | state: RouterStateSnapshot): Promise { 42 | let flag = false; 43 | try { 44 | await this.whoAmI(); 45 | let configData = await this.configService.getConfig(); 46 | 47 | if (configData.eula_accepted) { 48 | flag = true; 49 | } else { 50 | this.redirectToEULA(); 51 | } 52 | return flag; 53 | } catch (e) { 54 | this.redirectIfSessionNotFound(); 55 | return false; 56 | } 57 | } 58 | 59 | public async hasNoSession( 60 | route: ActivatedRouteSnapshot, 61 | state: RouterStateSnapshot): Promise { 62 | try { 63 | this.currentUser = new User(await this.whoAmI()); 64 | if (this.currentUser) { 65 | this.redirectIfSessionExists(decodeURIComponent(route.params['fwd'] ? route.params['fwd'] : '')); 66 | } 67 | return false; 68 | } catch (e) { 69 | return true; 70 | } 71 | } 72 | 73 | private redirectIfSessionExists(fwdUrl = '') { 74 | this.router.navigateByUrl(fwdUrl || this.fwdUrl); 75 | } 76 | 77 | private redirectIfSessionNotFound() { 78 | this.router.navigateByUrl('/base/login/' + encodeURIComponent(window.location.pathname)); 79 | } 80 | 81 | private redirectToEULA() { 82 | this.router.navigate(['/base/eula', { fwdUrl: encodeURIComponent(window.location.pathname) }]); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ui/src/app/services/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, URLSearchParams } from '@angular/http'; 3 | import { Bucket, Report } from '../model'; 4 | import { Config } from '../model/config.model'; 5 | 6 | @Injectable() 7 | export class ConfigService { 8 | 9 | constructor(private http: Http) { 10 | // do something 11 | } 12 | 13 | public async getReportConfig(): Promise { 14 | return await this.http.get(`/reports`).map((res) => res.json().data[0]).toPromise(); 15 | } 16 | 17 | 18 | public async createReportConfig(data: Report): Promise { 19 | return await this.http.post('/reports', data).map((res) => { 20 | return res.json(); 21 | }).toPromise(); 22 | } 23 | 24 | public async updateReportConfig(id: string, data: Report): Promise { 25 | return await this.http.put(`/reports/${id}`, data).map((res) => { 26 | return res.json(); 27 | }).toPromise(); 28 | } 29 | 30 | public async deleteBucketFromReport(reportId: string, id: string): Promise { 31 | return await this.http.delete(`/reports/${reportId}/buckets/${id}`).map((res) => { 32 | return res.json(); 33 | }).toPromise(); 34 | } 35 | 36 | public async createBucket(reportId: string, data: Bucket): Promise { 37 | return await this.http.post(`/reports/${reportId}/buckets`, data).map((res) => { 38 | return res.json(); 39 | }).toPromise(); 40 | } 41 | 42 | public async updateBucket(reportId: string, id: string, data: Bucket): Promise { 43 | return await this.http.put(`/reports/${reportId}/buckets/${id}`, data).map((res) => { 44 | return res.json(); 45 | }).toPromise(); 46 | } 47 | 48 | public async getConfig(): Promise { 49 | return await this.http.get(`/config`).map((res) => res.json()).toPromise(); 50 | } 51 | 52 | public async updateConfig(data: Config): Promise { 53 | return await this.http.put(`/config`, data).map((res) => res.json()).toPromise(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ui/src/app/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication.service'; 2 | export * from './user.service'; 3 | export * from './reporting.service'; 4 | export * from './config.service'; 5 | 6 | import { NgModule } from '@angular/core'; 7 | import { AuthenticationService, UserService, ReportingService, ConfigService } from '../services'; 8 | 9 | @NgModule({ 10 | providers: [ 11 | AuthenticationService, 12 | UserService, 13 | ReportingService, 14 | ConfigService, 15 | ], 16 | }) 17 | export class ServicesModule { 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/app/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Jsonp, Http } from '@angular/http'; 3 | 4 | import { User } from '../model'; 5 | import { ChangePasswordData } from '../+user-management/change-password/change-password-data'; 6 | 7 | @Injectable() 8 | export class UserService { 9 | 10 | constructor(private jsonp: Jsonp, private http: Http) { 11 | } 12 | 13 | public async changePassword(changePasswordData: ChangePasswordData): Promise { 14 | return await this.http.put('/account', { password: changePasswordData.newPassword, 15 | current_password: changePasswordData.currentPassword }).map((res) => res.json()).toPromise(); 16 | } 17 | 18 | public getUser(idForUser: number): Promise { 19 | return new Promise((resolve) => { 20 | resolve(new User({ 21 | firstName: 'Foo', 22 | lastName: 'Bar', 23 | email: 'foo@bar.com', 24 | status: 2, 25 | id: idForUser, 26 | })); 27 | }); 28 | } 29 | 30 | public putUser(idForUser: number, user: User): Promise { 31 | return new Promise((resolve) => { 32 | resolve(new User({ 33 | firstName: 'Foo', 34 | lastName: 'Bar', 35 | email: 'foo@bar.com', 36 | status: 2, 37 | id: idForUser, 38 | })); 39 | }); 40 | } 41 | 42 | public resetPassword(resetPassword: any): Promise { 43 | return new Promise((resolve) => { 44 | resolve(new User({ 45 | firstName: 'Foo', 46 | lastName: 'Bar', 47 | email: 'foo@bar.com', 48 | status: 2, 49 | id: 1, 50 | })); 51 | }); 52 | } 53 | 54 | public confirmResetPassword(token: string, password: string): Promise { 55 | return new Promise((resolve) => { 56 | resolve(new User({ 57 | firstName: 'Foo', 58 | lastName: 'Bar', 59 | email: 'foo@bar.com', 60 | status: 2, 61 | id: 1, 62 | })); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ui/src/app/settings/index.ts: -------------------------------------------------------------------------------- 1 | export { SettingsComponent } from './settings.component'; 2 | -------------------------------------------------------------------------------- /ui/src/app/settings/settings.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
    4 |
  • 5 | Report Setup 6 |
  • 7 |
  • 8 | Accounts Alias 9 |
  • 10 |
  • 11 | SSL Keys 12 |
  • 13 |
  • 14 | Change Password 15 |
  • 16 |
  • 17 | About 18 |
  • 19 |
20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 |
29 |
30 |

About

31 |
32 |
33 | Version: 34 |
35 |
36 | {{appVersion}} 37 |
38 |
39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 |
52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /ui/src/app/settings/settings.component.scss: -------------------------------------------------------------------------------- 1 | @import './../../assets/css/config'; 2 | 3 | -------------------------------------------------------------------------------- /ui/src/app/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'axc-settings', 5 | templateUrl: './settings.component.html', 6 | styles: [ 7 | require('./settings.component.scss'), 8 | ], 9 | }) 10 | export class SettingsComponent { 11 | public appVersion: string = `${process.env.VERSION}`; 12 | public settingType: string = 'SETUP'; 13 | 14 | public setTab(type: string) { 15 | this.settingType = type; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/assets/css/_base.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | font: 16px/1.23 'Heebo', sans-serif; 8 | background-color: $ax-color-gray-3; 9 | } 10 | 11 | .ax-container { 12 | max-width: $ax-width-container; 13 | margin: 0 auto; 14 | padding: 0 20px; 15 | } 16 | 17 | .text-left { 18 | text-align: left; 19 | } 20 | 21 | .text-center { 22 | text-align: center; 23 | } 24 | 25 | .text-right { 26 | text-align: right; 27 | } 28 | 29 | .pull-left { 30 | float: left; 31 | } 32 | 33 | .pull-right { 34 | float: right; 35 | } 36 | 37 | .cursor { 38 | cursor: pointer; 39 | } 40 | 41 | .margin-left-regular { 42 | margin-left: 1em; 43 | } 44 | 45 | .margin-top-regular { 46 | margin-top: 1em; 47 | } 48 | 49 | .margin-bottom-regular { 50 | margin-bottom: 1em; 51 | } 52 | 53 | .margin-bottom-large { 54 | margin-bottom: 3em; 55 | } 56 | .margin-top-large { 57 | margin-top: 3em; 58 | } 59 | 60 | .capitalize { 61 | text-transform: capitalize; 62 | } 63 | 64 | a, a:hover { 65 | color: $ax-color-teal-6; 66 | } 67 | 68 | .hide { 69 | display: none; 70 | } 71 | 72 | .show { 73 | display: block; 74 | } 75 | 76 | .dib{ 77 | display: inline-block; 78 | } 79 | -------------------------------------------------------------------------------- /ui/src/assets/css/_config.scss: -------------------------------------------------------------------------------- 1 | // Colors and font sizes defined in https://applatix.atlassian.net/wiki/display/UX/Typography 2 | 3 | $ax-color-white: #fff; 4 | $ax-color-teal: #00BDCE; 5 | $ax-color-gray: #778E94; 6 | $ax-color-dark-gray: #475458; 7 | $ax-storm-dust: #626362; 8 | $ax-color-dark-teal: #07656E; 9 | $ax-color-light-border: #c6cfd4; 10 | $ax-color-bg: #fff; 11 | $ax-intro-layout-bg: #f1f1f1; 12 | $ax-color-failed: #dd2c00; 13 | $ax-color-success: #4daf51; 14 | $ax-color-warning: #f4c01f; 15 | 16 | // To be documented: 17 | $ax-color-base-mlight: #c0eef1; // medium light 18 | $ax-color-base-slight: #e7f9fa; // super light 19 | $ax-color-base-dlight: #46888e; // dark light 20 | 21 | // colors 22 | $ax-color-base: $ax-color-teal; 23 | $ax-color-base-2: $ax-storm-dust; 24 | $ax-color-base-strong: $ax-color-dark-teal; 25 | $ax-color-bg-strong: $ax-color-light-border; 26 | $ax-color-secondary: $ax-storm-dust; 27 | 28 | $ax-color-bg-light: $ax-color-white; 29 | 30 | // Foundation settings 31 | $global-width: 100%; 32 | $dropdown-width: auto; 33 | $breakpoint-classes: (small medium large xlarge xxlarge); 34 | $body-font-family: 'Open Sans', sans-serif; 35 | 36 | $fa-font-path: 'assets/font-awesome/fonts'; 37 | 38 | // Z indexes 39 | $ax-zIndex-dropdown-menu: 1; 40 | $ax-zIndex-topbar: 2; 41 | $ax-zIndex-sliding-panel: 3; 42 | $ax-zIndex-notification: 4; 43 | 44 | // Button Colors 45 | $button-primary: #4A90E2; 46 | $button-primary-font: $ax-color-white; 47 | $button-default: $ax-color-white; 48 | $button-default-font: #4A90E2; 49 | 50 | // Others 51 | $ax-topbar-height: 80px; 52 | $ax-nav-width: 140px; 53 | $ax-width-container: 1284px; 54 | $ax-toolbar-height: 80px; 55 | 56 | // New colors 57 | $ax-color-gray-1: #F8FBFB; 58 | $ax-color-gray-2: #EFF3F5; 59 | $ax-color-gray-3: #DEE6EB; 60 | $ax-color-gray-4: #CCD6DD; 61 | $ax-color-gray-5: #8FA4B1; 62 | $ax-color-gray-6: #6D7F8B; 63 | $ax-color-gray-7: #495763; 64 | $ax-color-gray-8: #363C4A; 65 | $ax-color-gray-9: #000000; 66 | 67 | $ax-color-teal-1: #F5FBFD; 68 | $ax-color-teal-2: #DFF6F9; 69 | $ax-color-teal-3: #BDECF2; 70 | $ax-color-teal-4: #99E1EA; 71 | $ax-color-teal-5: #1FBDD0; 72 | $ax-color-teal-6: #00A2B3; 73 | $ax-color-teal-7: #006F8A; 74 | $ax-color-teal-8: #004C67; 75 | 76 | $ax-color-yellow: #FFD100; 77 | $ax-color-green: #7ED321; 78 | $ax-color-orange: #FF8046; 79 | $ax-color-red: #F00052; 80 | $ax-color-blue: #48A0FF; 81 | $ax-color-purple: #623C98; 82 | $ax-color-cyan: #3DE3FF; 83 | 84 | $ax-color-base-dark: #2b303a; 85 | 86 | $ax-color-error: #E96D76; 87 | $ax-color-success: #18BE94; 88 | $ax-color-progress: #0DADEA; 89 | -------------------------------------------------------------------------------- /ui/src/assets/css/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | 51 | a { 52 | text-decoration: none; 53 | } 54 | -------------------------------------------------------------------------------- /ui/src/assets/css/_typography.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: 2em; 3 | color: $ax-color-teal-7; 4 | } 5 | 6 | h2 { 7 | margin-bottom: 20px; 8 | font-size: 1.250em; 9 | color: $ax-color-teal-7; 10 | } 11 | 12 | h3 { 13 | margin-bottom: 16px; 14 | font-size: 1.250em; 15 | color: $ax-color-teal-7; 16 | } 17 | 18 | h4 { 19 | margin-bottom: 16px; 20 | font-size: 1em; 21 | color: $ax-color-teal-7; 22 | } 23 | 24 | p { 25 | margin-bottom: 16px; 26 | } 27 | 28 | small { 29 | font-size: .9em; 30 | } 31 | 32 | strong { 33 | font-weight: bold; 34 | } 35 | 36 | a { 37 | color: $ax-color-teal-7; 38 | 39 | &:hover { 40 | color: $ax-color-teal-6; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/src/assets/css/elements/_buttons.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/css/config'; 2 | 3 | .ax-btn { 4 | display: inline-block; 5 | padding: 0 18px; 6 | font-weight: bold; 7 | line-height: 38px; 8 | font-size: 0.8em; 9 | text-transform: uppercase; 10 | cursor: pointer; 11 | outline: 0; 12 | background-color: $button-default; 13 | color: $ax-color-teal-6; 14 | border: 1px solid #ddd; 15 | border-radius: 24px; 16 | 17 | &--base { 18 | color: $ax-color-gray-1; 19 | background-color: $ax-color-teal-6; 20 | 21 | &:not([disabled]):hover { 22 | background-color: $ax-color-teal-5; 23 | color: $ax-color-white; 24 | } 25 | } 26 | 27 | &--base-o { 28 | color: $ax-color-teal-6; 29 | background-color: transparent; 30 | border: 1px solid $ax-color-teal-4; 31 | } 32 | 33 | &--full-width { 34 | width: 100%; 35 | } 36 | 37 | &--wide { 38 | padding: 0 50px; 39 | } 40 | 41 | &--radius-2 { 42 | width: 36px; 43 | height: 36px; 44 | border-radius: 50%; 45 | border: 2px solid $ax-color-bg-strong; 46 | color: $ax-color-dark-gray; 47 | padding: 0; 48 | font-size: 15px; 49 | } 50 | } 51 | 52 | .ax-btn-radius-o { 53 | position: relative; 54 | display: inline-block; 55 | vertical-align: middle; 56 | width: 2em; 57 | height: 2em; 58 | border: 1px solid $ax-color-base-dlight; 59 | border-radius: 50%; 60 | outline: 0; 61 | cursor: pointer; 62 | 63 | &::before, 64 | &::after { 65 | position: absolute; 66 | top: 50%; 67 | left: 0.475em; 68 | width: 1em; 69 | height: 1px; 70 | background: $ax-color-base-strong; 71 | content: ''; 72 | } 73 | 74 | &--plus { 75 | &::before { 76 | transform: rotate(90deg); 77 | } 78 | } 79 | 80 | &--close { 81 | &::before { 82 | transform: rotate(45deg); 83 | } 84 | 85 | &::after { 86 | transform: rotate(-45deg); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /ui/src/assets/css/elements/_dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown-list { 2 | margin: 0; 3 | list-style: none; 4 | 5 | li { 6 | padding: 14px 20px; 7 | cursor: pointer; 8 | 9 | &:hover { 10 | background-color: $ax-color-bg; 11 | } 12 | } 13 | } 14 | 15 | .dropdown-anchor-border { 16 | position: relative; 17 | display: inline-block; 18 | overflow: hidden; 19 | padding: 12px 40px 12px 14px; 20 | color: $ax-color-base-strong; 21 | cursor: pointer; 22 | text-overflow: ellipsis; 23 | border: 1px solid $ax-color-base-dlight; 24 | border-radius: 26px; 25 | 26 | > i { 27 | margin-right: 6px; 28 | } 29 | 30 | &__arrow { 31 | position: absolute; 32 | right: 16px; 33 | top: 50%; 34 | transform: translateY(-50%); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ui/src/assets/css/elements/_list-group.scss: -------------------------------------------------------------------------------- 1 | .axc-list-group { 2 | margin: 0; 3 | padding: 2px; 4 | background-color: #fff; 5 | border-radius: 6px; 6 | list-style: none; 7 | 8 | &__item { 9 | padding: 8px 15px; 10 | font-weight: 400; 11 | font-size: 13px; 12 | color: $ax-color-gray-7; 13 | text-transform: uppercase; 14 | border-radius: 5px; 15 | cursor: pointer; 16 | 17 | &.active { 18 | color: #fff; 19 | background-color: $ax-color-teal-6; 20 | cursor: default; 21 | } 22 | 23 | &:not(.active):hover { 24 | background-color: $ax-color-gray-2; 25 | } 26 | 27 | &:not(:last-child) { 28 | margin-bottom: 2px; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/assets/css/elements/_table.scss: -------------------------------------------------------------------------------- 1 | .ax-table { 2 | width: 100%; 3 | table-layout: fixed; 4 | font-size: .85em; 5 | border-collapse: separate; 6 | border-spacing: 0px; 7 | 8 | th { 9 | padding: .75em 1.4em; 10 | color: $ax-color-base-strong; 11 | text-align: left; 12 | } 13 | 14 | td { 15 | padding: 0 1.4em; 16 | line-height: 80px; 17 | color: $ax-color-secondary; 18 | white-space: nowrap; 19 | text-overflow: ellipsis; 20 | overflow: hidden; 21 | vertical-align: middle; 22 | border-bottom: 1px solid $ax-color-bg-strong; 23 | border-top: 3px solid $ax-color-bg; 24 | } 25 | 26 | tbody tr { 27 | background-color: $ax-color-bg-light; 28 | 29 | &.selected { 30 | background-color: $ax-color-base-slight; 31 | } 32 | 33 | &:first-child td { 34 | border-top: 0; 35 | } 36 | } 37 | 38 | &--interactive { 39 | tbody tr:hover { 40 | background-color: $ax-color-base-slight; 41 | cursor: pointer; 42 | } 43 | } 44 | 45 | &--statuses { 46 | tbody tr { 47 | td:first-child { 48 | position: relative; 49 | 50 | &::before { 51 | position: absolute; 52 | left: 0; 53 | top: 0; 54 | bottom: -1px; 55 | width: 4px; 56 | content: ''; 57 | } 58 | } 59 | 60 | &.success td:first-child::before { 61 | background-color: $ax-color-success; 62 | } 63 | 64 | &.pending td:first-child::before { 65 | background-color: $ax-color-warning; 66 | } 67 | 68 | &.fail td:first-child::before { 69 | background-color: $ax-color-failed; 70 | } 71 | } 72 | } 73 | 74 | &--with-expaneding { 75 | tr:not(.expanded):nth-child(2n) { 76 | display: none; 77 | } 78 | 79 | tr.expanded:nth-child(2n - 1) td { 80 | border-bottom: 0; 81 | } 82 | 83 | tr:nth-child(2n) td { 84 | border-top: 0 !important; 85 | } 86 | } 87 | 88 | &--with-expaneding.ax-table--statuses { 89 | tbody tr { 90 | &.success + tr td:first-child::before { 91 | background-color: $ax-color-success; 92 | } 93 | 94 | &.pending + tr td:first-child::before { 95 | background-color: $ax-color-warning; 96 | } 97 | 98 | &.fail + tr td:first-child::before { 99 | background-color: $ax-color-failed; 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ui/src/assets/css/elements/_tag.scss: -------------------------------------------------------------------------------- 1 | .ax-tag { 2 | display: inline-block; 3 | margin: 0 5px 5px 0; 4 | padding: 2px; 5 | font-size: 12px; 6 | color: $ax-color-gray-6; 7 | background: $ax-color-gray-4; 8 | border-radius: 5px; 9 | 10 | &__name { 11 | display: inline-block; 12 | padding: 3px 4px; 13 | background: #fff; 14 | border-radius: 4px; 15 | } 16 | 17 | &__remove { 18 | display: inline-block; 19 | padding: 0 3px; 20 | cursor: pointer; 21 | 22 | &:hover { 23 | color: $ax-color-teal-6; 24 | } 25 | } 26 | 27 | &[hidden] { 28 | display: none; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/assets/css/elements/_toolbar.scss: -------------------------------------------------------------------------------- 1 | .ax-toolbar { 2 | padding: 0 0 20px; 3 | font-size: 16px; 4 | font-weight: bold; 5 | 6 | &::after, 7 | .ax-container::after { 8 | display: inline-block; 9 | vertical-align: middle; 10 | content: ""; 11 | } 12 | 13 | &__item { 14 | display: inline-block; 15 | vertical-align: middle; 16 | line-height: 1.5; 17 | color: $ax-color-teal-6; 18 | 19 | &:not(:last-child) { 20 | margin-right: 20px; 21 | } 22 | } 23 | 24 | &__button { 25 | padding: 0 16px; 26 | font-weight: 500; 27 | font-size: .875em; 28 | line-height: 40px; 29 | color: $ax-color-teal-7; 30 | text-transform: uppercase; 31 | background-color: $ax-color-gray-2; 32 | border-radius: 5px; 33 | cursor: pointer; 34 | outline: 0; 35 | 36 | &:hover { 37 | background-color: $ax-color-gray-1; 38 | } 39 | 40 | i { 41 | font-size: 1.2em; 42 | 43 | &:first-child { 44 | margin-right: 4px; 45 | } 46 | 47 | &:last-child { 48 | margin-left: 4px; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/src/assets/css/elements/_white-box.scss: -------------------------------------------------------------------------------- 1 | @import '../../../assets/css/config'; 2 | 3 | .white-box { 4 | background-color: $ax-color-bg-light; 5 | padding: 1.5em 2em; 6 | border-radius: 6px; 7 | border: 2px solid $ax-color-bg-strong; 8 | } 9 | -------------------------------------------------------------------------------- /ui/src/assets/css/main.scss: -------------------------------------------------------------------------------- 1 | @import './reset'; 2 | @import './config'; 3 | 4 | @import 'node_modules/font-awesome/scss/font-awesome'; 5 | @import 'node_modules/foundation-sites/scss/foundation'; 6 | 7 | @include foundation-global-styles; 8 | @include foundation-flex-classes; 9 | @include foundation-flex-grid; 10 | @include foundation-dropdown; 11 | @include foundation-typography; 12 | 13 | @import 'node_modules/c3/c3.min'; 14 | @import 'node_modules/jquery-date-range-picker/dist/daterangepicker.min'; 15 | @import './base'; 16 | @import './typography'; 17 | @import './elements/buttons'; 18 | @import './elements/dropdown'; 19 | @import './elements/form'; 20 | @import './elements/list-group'; 21 | @import './elements/table'; 22 | @import './elements/tag'; 23 | @import './elements/white-box'; 24 | @import './elements/toolbar'; 25 | -------------------------------------------------------------------------------- /ui/src/assets/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/assets/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /ui/src/assets/favicon/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/assets/favicon/android-chrome-256x256.png -------------------------------------------------------------------------------- /ui/src/assets/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/assets/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /ui/src/assets/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #603cba 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/src/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /ui/src/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /ui/src/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /ui/src/assets/favicon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "icons": [ 4 | { 5 | "src": "/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/android-chrome-256x256.png", 11 | "sizes": "256x256", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#363c4a", 16 | "background_color": "#363c4a", 17 | "display": "standalone" 18 | } -------------------------------------------------------------------------------- /ui/src/assets/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/assets/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /ui/src/assets/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 23 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ui/src/assets/icons/AXClaudia_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/assets/icons/AXClaudia_logo.png -------------------------------------------------------------------------------- /ui/src/assets/icons/full_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ui/src/assets/icons/ico_failed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | i_failed 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ui/src/assets/icons/ico_success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | i_complete 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ui/src/assets/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Applatix/claudia/8052d2cb5081c4ee9eb5773628500f050787ca8d/ui/src/assets/icons/logo.png -------------------------------------------------------------------------------- /ui/src/custom-typings.d.ts: -------------------------------------------------------------------------------- 1 | declare var ENV: string; 2 | declare var HMR: boolean; 3 | declare var System: SystemJS; 4 | 5 | interface SystemJS { 6 | import: (path?: string) => Promise; 7 | } 8 | 9 | interface GlobalEnvironment { 10 | ENV: string; 11 | HMR: boolean; 12 | SystemJS: SystemJS; 13 | System: SystemJS; 14 | } 15 | 16 | interface Es6PromiseLoader { 17 | (id: string): (exportName?: string) => Promise; 18 | } 19 | 20 | type FactoryEs6PromiseLoader = () => Es6PromiseLoader; 21 | type FactoryPromise = () => Promise; 22 | 23 | type AsyncRoutes = { 24 | [component: string]: Es6PromiseLoader | 25 | Function | 26 | FactoryEs6PromiseLoader | 27 | FactoryPromise 28 | }; 29 | 30 | 31 | type IdleCallbacks = Es6PromiseLoader | 32 | Function | 33 | FactoryEs6PromiseLoader | 34 | FactoryPromise; 35 | 36 | interface WebpackModule { 37 | hot: { 38 | data?: any, 39 | idle: any, 40 | accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void; 41 | decline(deps?: any | string | string[]): void; 42 | dispose(callback?: (data?: any) => void): void; 43 | addDisposeHandler(callback?: (data?: any) => void): void; 44 | removeDisposeHandler(callback?: (data?: any) => void): void; 45 | check(autoApply?: any, callback?: (err?: Error, outdatedModules?: any[]) => void): void; 46 | apply(options?: any, callback?: (err?: Error, outdatedModules?: any[]) => void): void; 47 | status(callback?: (status?: string) => void): void | string; 48 | removeStatusHandler(callback?: (status?: string) => void): void; 49 | }; 50 | } 51 | 52 | 53 | interface WebpackRequire { 54 | (id: string): any; 55 | (paths: string[], callback: (...modules: any[]) => void): void; 56 | ensure(ids: string[], callback: (req: WebpackRequire) => void, chunkName?: string): void; 57 | context(directory: string, useSubDirectories?: boolean, regExp?: RegExp): WebpackContext; 58 | } 59 | 60 | interface WebpackContext extends WebpackRequire { 61 | keys(): string[]; 62 | } 63 | 64 | interface ErrorStackTraceLimit { 65 | stackTraceLimit: number; 66 | } 67 | 68 | 69 | // Extend typings 70 | interface NodeRequire extends WebpackRequire { } 71 | interface ErrorConstructor extends ErrorStackTraceLimit { } 72 | interface NodeRequireFunction extends Es6PromiseLoader { } 73 | interface NodeModule extends WebpackModule { } 74 | interface Global extends GlobalEnvironment { } 75 | -------------------------------------------------------------------------------- /ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% if (webpackConfig.htmlElements.headTags) { %> 18 | 19 | <%= webpackConfig.htmlElements.headTags %> 20 | <% } %> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Loading... 29 | 30 | 31 | <% if (htmlWebpackPlugin.options.metadata.isDevServer && htmlWebpackPlugin.options.metadata.HMR !== true) { %> 32 | 33 | 34 | <% } %> 35 | 36 | 37 | -------------------------------------------------------------------------------- /ui/src/main.browser.ts: -------------------------------------------------------------------------------- 1 | import 'rxjs/Rx'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { decorateModuleRef } from './app/environment'; 4 | import { bootloader } from '@angularclass/hmr'; 5 | import { AppModule } from './app'; 6 | 7 | export function main(): Promise { 8 | return platformBrowserDynamic() 9 | .bootstrapModule(AppModule) 10 | .then(decorateModuleRef) 11 | .catch(err => console.error(err)); 12 | } 13 | 14 | bootloader(main); 15 | -------------------------------------------------------------------------------- /ui/src/polyfills.browser.ts: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | require('babel-polyfill'); 3 | 4 | import 'core-js/es6/symbol'; 5 | import 'core-js/es6/object'; 6 | import 'core-js/es6/function'; 7 | import 'core-js/es6/parse-int'; 8 | import 'core-js/es6/parse-float'; 9 | import 'core-js/es6/number'; 10 | import 'core-js/es6/math'; 11 | import 'core-js/es6/string'; 12 | import 'core-js/es6/date'; 13 | import 'core-js/es6/array'; 14 | import 'core-js/es6/regexp'; 15 | import 'core-js/es6/map'; 16 | import 'core-js/es6/set'; 17 | import 'core-js/es6/weak-map'; 18 | import 'core-js/es6/weak-set'; 19 | import 'core-js/es6/typed'; 20 | import 'core-js/es6/reflect'; 21 | import 'core-js/es7/reflect'; 22 | import 'zone.js/dist/zone'; 23 | import 'ts-helpers'; 24 | 25 | if ('production' !== ENV) { 26 | Error.stackTraceLimit = Infinity; 27 | /* tslint:disable */ 28 | require('zone.js/dist/long-stack-trace-zone'); 29 | /* tslint:enable */ 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/vendor.browser.ts: -------------------------------------------------------------------------------- 1 | let $ = require('jquery'); 2 | window['$'] = $; 3 | window['jQuery'] = $; 4 | 5 | let moment = require('moment'); 6 | window['moment'] = moment; 7 | import 'd3/d3.min'; 8 | import 'c3/c3.min'; 9 | 10 | import '@angular/platform-browser'; 11 | import '@angular/platform-browser-dynamic'; 12 | import '@angular/core'; 13 | import '@angular/common'; 14 | import '@angular/forms'; 15 | import '@angular/http'; 16 | import '@angular/router'; 17 | 18 | import '@angularclass/hmr'; 19 | 20 | import 'rxjs/add/operator/map'; 21 | import 'rxjs/add/operator/mergeMap'; 22 | import 'jquery-date-range-picker/dist/jquery.daterangepicker.min'; 23 | 24 | // import 'nvd3/build/nv.d3'; 25 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "sourceMap": true, 10 | "noEmitHelpers": true, 11 | "strictNullChecks": false, 12 | "baseUrl": "./src", 13 | "paths": { 14 | }, 15 | "lib": [ 16 | "dom", 17 | "es6" 18 | ], 19 | "types": [ 20 | "hammerjs", 21 | "node", 22 | "source-map", 23 | "uglify-js", 24 | "webpack", 25 | "jquery" 26 | ] 27 | }, 28 | "exclude": [ 29 | "node_modules", 30 | "dist" 31 | ], 32 | "awesomeTypescriptLoaderOptions": { 33 | "forkChecker": true, 34 | "useWebpackText": true 35 | }, 36 | "compileOnSave": false, 37 | "buildOnSave": false, 38 | "atom": { "rewriteTsconfig": false } 39 | } 40 | -------------------------------------------------------------------------------- /ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rulesDirectory": [ 4 | "node_modules/codelyzer" 5 | ], 6 | "rules": { 7 | "quotemark": [ 8 | true, 9 | "single" 10 | ], 11 | "no-string-literal": false, 12 | "interface-name": [ 13 | false, 14 | "always-prefix" 15 | ], 16 | "no-var-requires": false, 17 | "object-literal-sort-keys": false, 18 | "max-line-length": [ 19 | true, 20 | 180 21 | ], 22 | "ordered-imports": [ 23 | false 24 | ], 25 | "component-selector-name": [ 26 | true, 27 | "kebab-case" 28 | ], 29 | "directive-selector-type": [ 30 | true, 31 | "attribute" 32 | ], 33 | "component-selector-type": [ 34 | true, 35 | "element" 36 | ], 37 | "directive-selector-prefix": [ 38 | true, 39 | "axc" 40 | ], 41 | "component-selector-prefix": [ 42 | true, 43 | "axc" 44 | ], 45 | "pipe-naming": [ 46 | true, 47 | "camelCase" 48 | ], 49 | "use-input-property-decorator": true, 50 | "use-output-property-decorator": true, 51 | "use-host-property-decorator": true, 52 | "no-attribute-parameter-decorator": true, 53 | "no-input-rename": false, 54 | "no-output-rename": true, 55 | "no-forward-ref": true, 56 | "use-life-cycle-interface": true, 57 | "use-pipe-transform-interface": true, 58 | "component-class-suffix": true, 59 | "variable-name": false, 60 | "directive-class-suffix": true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ui/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "modules", 3 | "out": "doc", 4 | "theme": "default", 5 | "ignoreCompilerErrors": "true", 6 | "experimentalDecorators": "true", 7 | "emitDecoratorMetadata": "true", 8 | "target": "ES5", 9 | "moduleResolution": "node", 10 | "preserveConstEnums": "true", 11 | "stripInternal": "true", 12 | "suppressExcessPropertyErrors": "true", 13 | "suppressImplicitAnyIndexErrors": "true", 14 | "module": "commonjs" 15 | } 16 | -------------------------------------------------------------------------------- /ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | switch (process.env.NODE_ENV) { 2 | case 'prod': 3 | case 'production': 4 | module.exports = require('./config/webpack.prod')({env: 'production'}); 5 | break; 6 | case 'test': 7 | case 'testing': 8 | module.exports = require('./config/webpack.test')({env: 'test'}); 9 | break; 10 | default: 11 | module.exports = require('./config/webpack.dev')({env: 'development'}); 12 | } 13 | -------------------------------------------------------------------------------- /userdb/schema.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Applatix, Inc. 2 | package userdb 3 | 4 | // SchemaVersion is the user database schema version of this version of the app 5 | const SchemaVersion = 1 6 | 7 | var schemaV1 = []string{` 8 | -- single row table to store configuration & system information 9 | CREATE TABLE configuration ( 10 | id INT NOT NULL PRIMARY KEY DEFAULT 1, 11 | schema_version INT NOT NULL, 12 | session_auth_key BYTEA NOT NULL, 13 | session_crypt_key BYTEA NOT NULL, 14 | private_key TEXT NOT NULL, 15 | public_certificate TEXT NOT NULL, 16 | eula_accepted BOOLEAN NOT NULL DEFAULT false, 17 | CONSTRAINT single_row CHECK (id = 1) 18 | ); 19 | `, ` 20 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 21 | `, ` 22 | CREATE EXTENSION IF NOT EXISTS "citext"; 23 | `, ` 24 | CREATE TABLE appuser ( 25 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 26 | ctime TIMESTAMP NOT NULL DEFAULT current_timestamp, 27 | mtime TIMESTAMP NOT NULL DEFAULT current_timestamp, 28 | username CITEXT NOT NULL UNIQUE, 29 | password_hash TEXT NOT NULL 30 | ); 31 | `, ` 32 | CREATE TABLE report ( 33 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 34 | ctime TIMESTAMP NOT NULL DEFAULT current_timestamp, 35 | mtime TIMESTAMP NOT NULL DEFAULT current_timestamp, 36 | report_name CITEXT NOT NULL, 37 | retention_days INT NOT NULL, 38 | status TEXT NOT NULL, 39 | status_detail TEXT NOT NULL, 40 | -- currently only one report per user is supported. remove unique constraint when this restriction is lifted 41 | owner_user_id UUID NOT NULL UNIQUE REFERENCES appuser(id) ON DELETE CASCADE 42 | ); 43 | `, ` 44 | CREATE TABLE bucket ( 45 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 46 | ctime TIMESTAMP NOT NULL DEFAULT current_timestamp, 47 | report_id UUID NOT NULL REFERENCES report(id) ON DELETE CASCADE, 48 | bucketname TEXT NOT NULL, 49 | region TEXT NOT NULL, 50 | report_path TEXT NOT NULL, 51 | aws_access_key_id TEXT NOT NULL, 52 | aws_secret_access_key TEXT NOT NULL, 53 | CONSTRAINT unique_s3path UNIQUE (bucketname, report_path) 54 | ); 55 | `, ` 56 | -- Table of aws account ids associated with a report. Used primarily translating aws account ids to user defined display names 57 | CREATE TABLE aws_account ( 58 | report_id UUID NOT NULL REFERENCES report(id) ON DELETE CASCADE, 59 | aws_account_id TEXT NOT NULL, 60 | name TEXT NOT NULL, 61 | CONSTRAINT unique_account UNIQUE (report_id, aws_account_id) 62 | ); 63 | `, ` 64 | -- Table of product codes mapped to AWS product information about the product. This is global information, not tied to a specific user 65 | CREATE TABLE aws_product ( 66 | product_code TEXT NOT NULL UNIQUE, 67 | name TEXT NOT NULL, 68 | description TEXT NOT NULL 69 | ); 70 | `, 71 | } 72 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Applatix, Inc. 2 | package claudia 3 | 4 | import "fmt" 5 | 6 | // Version information set by link flags during build 7 | var ( 8 | Version = "unknown" 9 | Revision = "unknown" 10 | FullVersion = fmt.Sprintf("%s-%s", Version, Revision) 11 | BuildDate = "unknown" 12 | DisplayVersion = fmt.Sprintf("%s (Build Date: %s)", FullVersion, BuildDate) 13 | ) 14 | --------------------------------------------------------------------------------