Merge branch 'master' into issues/3096

This commit is contained in:
Yu Cao
2019-01-08 10:18:35 -05:00
committed by GitHub
5475 changed files with 1142704 additions and 89039 deletions
+204
View File
@@ -0,0 +1,204 @@
version: 2
defaults: &defaults
working_directory: /go/src/github.com/weaveworks/scope
docker:
- image: weaveworks/scope-backend-build:master-fda40b83
client-defaults: &client-defaults
working_directory: /home/weave/scope
docker:
- image: weaveworks/scope-ui-build:master-c0b60a16
workflows:
version: 2
test_and_deploy:
jobs:
- lint
- unit-test
- client-build
- client-test:
requires:
- client-build
- xplatform-build:
requires:
- build
- build:
requires:
- client-build
- integration-tests:
requires:
- lint
- unit-test
- build
- deploy:
filters:
branches:
only: master
requires:
- client-test
- integration-tests
jobs:
lint:
<<: *defaults
steps:
- checkout
- run: make BUILD_IN_CONTAINER=false lint
unit-test:
<<: *defaults
parallelism: 1
steps:
- checkout
- run: COVERDIR=./coverage make BUILD_IN_CONTAINER=false CODECGEN_UID=23 tests
- persist_to_workspace:
root: .
paths:
- coverage
# Create client/build/index.html
client-build:
<<: *client-defaults
steps:
- checkout
- restore_cache:
name: Restoring Yarn Cache
key: yarn-cache-2-{{ checksum "client/yarn.lock" }}
- restore_cache:
name: Restoring client/node_modules
key: node-modules-{{ checksum "client/yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
- run: cd client; yarn install
- save_cache:
name: Saving Yarn Cache
key: yarn-cache-2-{{ checksum "client/yarn.lock" }}
paths:
- "/home/weave/scope/.cache/yarn"
- save_cache:
name: Saving client/node_modules
# include the CI config in the checksum because it will change when the docker image changes
key: node-modules-{{ checksum "client/yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
paths:
- "/home/weave/scope/client/node_modules"
- run: |
cd client
yarn run build
yarn run build-external
yarn run bundle
- persist_to_workspace:
root: /home/weave/scope
paths:
- client/build/
- client/build-external/
- client/bundle/weave-scope.tgz
client-test:
<<: *client-defaults
steps:
- checkout
- restore_cache:
name: Restoring Yarn Cache
key: yarn-cache-2-{{ checksum "client/yarn.lock" }}
- restore_cache:
name: Restoring client/node_modules
key: node-modules-{{ checksum "client/yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
- run: |
cd client
yarn install
yarn run lint
yarn test
xplatform-build:
<<: *defaults
steps:
- checkout
- run: GOARCH=arm make BUILD_IN_CONTAINER=false GO_BUILD_INSTALL_DEPS= prog/scope
- run: GOOS=darwin make BUILD_IN_CONTAINER=false GO_BUILD_INSTALL_DEPS= prog/scope
build:
<<: *defaults
steps:
- checkout
- setup_remote_docker
- attach_workspace:
at: .
- run: make BUILD_IN_CONTAINER=false SUDO= static all
- run: cd extras; make BUILD_IN_CONTAINER=false
- run: make -C tools/runner
- persist_to_workspace:
root: .
paths:
- scope.tar
- cloud-agent.tar
- tools/runner/runner
- prog/externalui/
- prog/staticui/
- report/report.codecgen.go
- render/detailed/detailed.codecgen.go
integration-tests:
machine:
image: circleci/classic:201709-01
working_directory: /home/circleci/src/github.com/weaveworks/scope
environment:
CIRCLE_ARTIFACTS: /tmp/artifacts
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
GOPATH: /home/circleci/
parallelism: 2
steps:
- checkout
- attach_workspace:
at: .
- run: |
sudo apt-get update
sudo apt-get install python-pip jq pv
- run: mkdir $CIRCLE_ARTIFACTS
# kick off creation of test VMs
- run: test -z "$SECRET_PASSWORD" || bin/setup-circleci-secrets "$SECRET_PASSWORD"
- run: test -z "$SECRET_PASSWORD" || (cd integration; ./gce.sh make_template)
- run: test -z "$SECRET_PASSWORD" || (cd integration; ./gce.sh setup && eval $(./gce.sh hosts); ./setup.sh)
- run: make deps; touch tools/runner/runner
# Run all integration tests
- run:
command: test -z "$SECRET_PASSWORD" || (cd integration; eval $(./gce.sh hosts); ./run_all.sh)
no_output_timeout: 5m
# Destroy testing VMs:
- run:
command: test -z "$SECRET_PASSWORD" || (cd integration; ./gce.sh destroy)
background: true
# Code coverage
- run: ./tools/cover/gather_coverage.sh ./coverage
- run: goveralls -repotoken $COVERALLS_REPO_TOKEN -coverprofile=profile.cov -service=circleci
- run: cp coverage.* */*.codecgen.go $CIRCLE_ARTIFACTS
- store_artifacts:
path: /tmp/artifacts
deploy:
<<: *defaults
environment:
IMAGES: scope cloud-agent
steps:
- checkout
- setup_remote_docker
- attach_workspace:
at: .
- run: |
pip install awscli
docker load -i scope.tar
docker load -i cloud-agent.tar
- run: |
test -z "${DOCKER_USER}" && exit 0
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
for IMAGE in $IMAGES; do
test "${DOCKER_ORGANIZATION:-$DOCKER_USER}" = "weaveworks" || docker tag weaveworks/$IMAGE:latest ${DOCKER_ORGANIZATION:-$DOCKER_USER}/$IMAGE:latest
docker tag weaveworks/$IMAGE:latest ${DOCKER_ORGANIZATION:-$DOCKER_USER}/$IMAGE:$(./tools/image-tag)
docker push ${DOCKER_ORGANIZATION:-$DOCKER_USER}/$IMAGE:latest
docker push ${DOCKER_ORGANIZATION:-$DOCKER_USER}/$IMAGE:$(./tools/image-tag)
done
- run: |
test -z "${QUAY_USER}" && exit 0
docker login -e '.' -u "$QUAY_USER" -p "$QUAY_PASSWORD" quay.io
docker tag weaveworks/scope:$(./tools/image-tag) "quay.io/${QUAY_ORGANIZATION}/scope:$(./tools/image-tag)"
docker push "quay.io/${QUAY_ORGANIZATION}/scope:$(./tools/image-tag)"
- run: test -z "${UI_BUCKET_KEY_ID}" || (make BUILD_IN_CONTAINER=false ui-upload ui-pkg-upload)
+58
View File
@@ -0,0 +1,58 @@
<!--
Hi, thank you for opening an issue!
Before hitting the button...
** Is this a REQUEST FOR HELP? **
If so, please have a look at:
- How WeaveScope works : https://www.weave.works/docs/scope/latest/how-it-works/
- our troubleshooting page: https://www.weave.works/docs/scope/latest/building/
- our help page, to choose the best channel (Slack, etc.) to reach out: https://weave-community.slack.com/messages/scope/
** Is this a FEATURE REQUEST? **
If so, please search existing feature requests, and if you find a similar one, up-vote it and/or add your comments to it instead.
If you did not find a similar one, please describe in details:
- why: your use-case, specific constraints you may have, etc.
- what: the feature/behaviour/change you would like to see in Weave Scope
Do not hesitate, when appropriate, to share the exact commands or API you would like, and/or to share a diagram (e.g.: asciiflow.com): "a picture is worth a thousand words".
** Is this a BUG REPORT? **
Please fill in as much of the template below as you can.
Thank you!
-->
## What you expected to happen?
## What happened?
<!-- Error message, actual behaviour, etc. -->
## How to reproduce it?
<!-- Specific steps, as minimally and precisely as possible. -->
## Anything else we need to know?
<!-- Cloud provider? Hardware? How did you configure your cluster? Kubernetes YAML, KOPS, etc. -->
## Versions:
<!-- Please paste in the output of these commands; 'kubectl' only if using Kubernetes -->
```
$ scope version
$ docker version
$ uname -a
$ kubectl version
```
## Logs:
```
$ docker logs weavescope
```
or, if using Kubernetes:
```
$ kubectl logs <weave-scope-pod> -n <namespace>
```
<!-- (If output is long, please consider a Gist.) -->
<!-- Anything interesting or unusual output by the below, potentially relevant, commands?
$ journalctl -u docker.service --no-pager
$ journalctl -u kubelet --no-pager
$ kubectl get events
-->
+2
View File
@@ -9,6 +9,7 @@ _test
.vagrant
releases
tmp
.cache
# Architecture specific extensions/prefixes
*.[568vq]
@@ -62,6 +63,7 @@ client/build-external/*
prog/staticui/*
prog/externalui/*
client/build-pkg
client/bundle
# Website
site-build
+326
View File
@@ -1,3 +1,329 @@
## Release 1.10.1
This is a re-release of 1.10.0 which got hit by an unfortunate build
error.
- UI Build: stop deleting static ui files when building external ui
[#3439](https://github.com/weaveworks/scope/pull/3439)
## Release 1.10.0
Highlights:
- Add Kubernetes Persistent Volume snapshot and clone operations
[#3355](https://github.com/weaveworks/scope/pull/3355)
- Kubernetes objects can be reported just once in a cluster, instead
of reporting the same data from every node.
[#3274](https://github.com/weaveworks/scope/pull/3274)
[#3419](https://github.com/weaveworks/scope/pull/3419)
[#3432](https://github.com/weaveworks/scope/pull/3432)
- App now supports http basic auth
[#3393](https://github.com/weaveworks/scope/pull/3393)
Some changes (#3266, #3272) were made to the wire protocol, which
means new probes are not compatible with an older app.
Thanks for contributions from @Akash4927, @akshatnitd, @bhavin192,
@hexmind, @gfeun, @gotjosh, @gruebel, @hexmind, @jgsqware, @ltachet,
@muthumalla, @rvrvrv, @satyamz, @ScottBrenner, @ssiddhantsharma,
@visualapps, @WhiteHatTux, @ycao56, @Xivolkar - some of these came via
[Hacktoberfest](https://hacktoberfest.digitalocean.com/).
Performance:
- Probe: use netlink to talk to conntrack
[#3298](https://github.com/weaveworks/scope/pull/3298)
- Remove First and Last data members from Metrics structs
[#3266](https://github.com/weaveworks/scope/pull/3266)
- Remove old 'Controls' field which was replaced two years ago
[#3272](https://github.com/weaveworks/scope/pull/3272)
- Probe: Don't report dead or defunct processes
[#3379](https://github.com/weaveworks/scope/pull/3379)
- Simplify fetch of IP addresses in a namespace
[#3335](https://github.com/weaveworks/scope/pull/3335)
- Discard pod updates for other nodes
[#3391](https://github.com/weaveworks/scope/pull/3391)
- Probe: Rate-limit report publishing
[#3386](https://github.com/weaveworks/scope/pull/3386)
- In multitenant app, drop all nodes for big topologies
[#3384](https://github.com/weaveworks/scope/pull/3384)
Bug fixes and minor improvements:
- Initial Container Runtime Interface (CRI) support
[#3275](https://github.com/weaveworks/scope/pull/3275)
[#3305](https://github.com/weaveworks/scope/pull/3305)
[#3308](https://github.com/weaveworks/scope/pull/3308)
[#3392](https://github.com/weaveworks/scope/pull/3392)
[#3364](https://github.com/weaveworks/scope/pull/3364)
- Add storage driver name to Persistent Volume
[#3260](https://github.com/weaveworks/scope/pull/3260)
- Fix WithLatests() fixup on duplicate keys
[#3281](https://github.com/weaveworks/scope/pull/3281)
- Add EKS variant of 'pause container'
[#3421](https://github.com/weaveworks/scope/pull/3421)
- Add Opentracing (Jaeger) distributed tracing for profiling the app
[#3307](https://github.com/weaveworks/scope/pull/3307)
[#3380](https://github.com/weaveworks/scope/pull/3380)
[#3383](https://github.com/weaveworks/scope/pull/3383)
[#3325](https://github.com/weaveworks/scope/pull/3325)
- app: update stopped container message
[#3396](https://github.com/weaveworks/scope/pull/3396)
- Example Kubernetes yaml files: fix typos and work with newer Kubernetes
[#3403](https://github.com/weaveworks/scope/pull/3403)
- Example Kubernetes yaml files: add support for PodSecurityPolicy
[#3354](https://github.com/weaveworks/scope/pull/3354)
- Example Kubernetes yaml files: add rules in cluster role for storage components
[#3290](https://github.com/weaveworks/scope/pull/3290)
- rename 'storagesheet' to 'sheet' in reports
[#3323](https://github.com/weaveworks/scope/pull/3323)
[#3324](https://github.com/weaveworks/scope/pull/3324)
- Check container is running before trying to open its namespace
[#3279](https://github.com/weaveworks/scope/pull/3279)
User Interface
- Upgrade to font-awesome 5 and new icons
[#3426](https://github.com/weaveworks/scope/pull/3426)
- replace share icon with sitemap on graph button
[#3387](https://github.com/weaveworks/scope/pull/3387)
- Bump ui-components version
[#3282](https://github.com/weaveworks/scope/pull/3282)
[#3431](https://github.com/weaveworks/scope/pull/3431)
- Use GraphNode component from ui-components library
[#3262](https://github.com/weaveworks/scope/pull/3262)
- Make header semitransparent
[#3294](https://github.com/weaveworks/scope/pull/3294)
- Fix broken styling of terminal in contrast mode
[#3347](https://github.com/weaveworks/scope/pull/3347)
- Add onRouteChange hook to Scope app
[#3349](https://github.com/weaveworks/scope/pull/3349)
- Stop two Scope instances on the same domain from changing each other's history
[#3326](https://github.com/weaveworks/scope/pull/3326)
- Use new Search component from ui-components repo
[#3337](https://github.com/weaveworks/scope/pull/3337)
- Update localStorage with Scope state also on initial router hook
[#3315](https://github.com/weaveworks/scope/pull/3315)
Build and test improvements
- Remove weaveutil and weave from Dockerfile.cloud-agent
[#3369](https://github.com/weaveworks/scope/pull/3369)
- Build on Power CPU architecture.
[#3231](https://github.com/weaveworks/scope/pull/3231)
- build: Fix import for golint which has moved
[#3389](https://github.com/weaveworks/scope/pull/3389)
- Sleep to stop TestRegistryDelete() failing
[#3334](https://github.com/weaveworks/scope/pull/3334)
- Fix vendoring of ugorji/go
[#3280](https://github.com/weaveworks/scope/pull/3280)
- Update version of sirupsen/logrus
[#3276](https://github.com/weaveworks/scope/pull/3276)
[#3277](https://github.com/weaveworks/scope/pull/3277)
- Upgrade Kubernetes client-go version to 8.0.0
[#3329](https://github.com/weaveworks/scope/pull/3329)
- Update lodash dependency to remove security warning
[#3310](https://github.com/weaveworks/scope/pull/3310)
- Rework UI build to improve caching and fix packaging issue
[#3353](https://github.com/weaveworks/scope/pull/3353)
[#3356](https://github.com/weaveworks/scope/pull/3356)
[#3360](https://github.com/weaveworks/scope/pull/3360)
[#3382](https://github.com/weaveworks/scope/pull/3382)
- Update the 'tools' subdirectory
[#3311](https://github.com/weaveworks/scope/pull/3311)
[#3312](https://github.com/weaveworks/scope/pull/3312)
- Clean up Dockerfiles
[#3411](https://github.com/weaveworks/scope/pull/3411)
- update vendored copy of tcptracer-bpf for licence reasons
[#3336](https://github.com/weaveworks/scope/pull/3336)
- vendor: update gopkg.in/yaml.v2 to latest upstream
[#3317](https://github.com/weaveworks/scope/pull/3317)
- Create bpf stop file differently, in integration test
[#3332](https://github.com/weaveworks/scope/pull/3332)
- Move to CircleCI 2.0
[#3333](https://github.com/weaveworks/scope/pull/3333)
## Release 1.9.1
Highlights:
Scope now displays Kubernetes Storage (PersistentVolume and
PersistentVolumeClaim) information on the Pods view.
[#3132](https://github.com/weaveworks/scope/pull/3132)
Thanks to @satyamz and all at OpenEBS for this contribution!
Also thanks for the example Kubernetes manifests from @tasdikrahman.
Bug fixes and minor improvements:
- Fix 'Unmanaged' nodes showing despite 'Hide Umanaged' filter
[#3189](https://github.com/weaveworks/scope/pull/3189)
- Fixes monospace font overlapping in terminal+linux
[#3248](https://github.com/weaveworks/scope/pull/3248)
- make process-by-name topology show something
[#3208](https://github.com/weaveworks/scope/pull/3208)
- Use the default value for a TopologyOption if omitted
[#3165](https://github.com/weaveworks/scope/pull/3165)
- Adjusted terminal character width/height estimation
[#3179](https://github.com/weaveworks/scope/pull/3179)
- Fix pause image detection for Kubernetes 1.10
[#3183](https://github.com/weaveworks/scope/pull/3183)
- ebpf: update check for known faulty Ubuntu kernels
[#3188](https://github.com/weaveworks/scope/pull/3188)
- Add option to print probe reports to stdout, for debugging
[#3204](https://github.com/weaveworks/scope/pull/3204)
- Fix querier panic introduced in #3143
[#3156](https://github.com/weaveworks/scope/pull/3156)
- Fix rare crash in filter function
[#3232](https://github.com/weaveworks/scope/pull/3232)
- Probe: fix error message to name the correct flag probe.proc.spy
[#3216](https://github.com/weaveworks/scope/pull/3216)
- Remove ProcessWithContainerNameRenderer, it wasn't working
[#3263](https://github.com/weaveworks/scope/pull/3263)
- Close terminal window on exit; update xterm to version 3.3.0
[#3172](https://github.com/weaveworks/scope/pull/3172)
- Add org.opencontainers.image.* labels to Dockerfiles
[#3171](https://github.com/weaveworks/scope/pull/3171)
- Make table header line up with columns when scrollbar appears
[#3169](https://github.com/weaveworks/scope/pull/3169)
- Add command-line flag to set SQS RPC timeout
[#3157](https://github.com/weaveworks/scope/pull/3157)
Performance:
A number of small performance improvements have gone into this
release, reducing memory and CPU usage.
- Probe: remove backwards-compatibility code when publishing reports
[#3215](https://github.com/weaveworks/scope/pull/3215)
- Optimise Node.WithLatests()
[#3268](https://github.com/weaveworks/scope/pull/3268)
- Optimise WithParents() when there is only one parent
[#3269](https://github.com/weaveworks/scope/pull/3269)
- Re-use gzip writers in a pool
[#3267](https://github.com/weaveworks/scope/pull/3267)
- Optimise merge where one side is a subset of the other
[#3253](https://github.com/weaveworks/scope/pull/3253)
- Use a buffer pool in report.ReadBinary() to reduce garbage-collection
[#3255](https://github.com/weaveworks/scope/pull/3255)
- Faster report merging through mutating objects
[#3236](https://github.com/weaveworks/scope/pull/3236)
- Faster path to check an IP address against known networks
[#3142](https://github.com/weaveworks/scope/pull/3142)
- Skip pods with no IP addresses when rendering network connections
[#3201](https://github.com/weaveworks/scope/pull/3201)
- Fetch container IPs directly from the namespace instead of calling 'weave ps'
[#3207](https://github.com/weaveworks/scope/pull/3207)
UI:
A number of changes adjusting fonts and colors, and standardising the
UI through the use of a theme.
- Update fonts - use Proxima Nova as a default font instead of Roboto.
[#3177](https://github.com/weaveworks/scope/pull/3177)
- Adjust font sizes
[#3181](https://github.com/weaveworks/scope/pull/3181)
- Update gray theme colors
[#3234](https://github.com/weaveworks/scope/pull/3234)
- Use new accent theme colors
[#3230](https://github.com/weaveworks/scope/pull/3230)
- Use new purple theme colors
[#3229](https://github.com/weaveworks/scope/pull/3229)
- Use new theme gray colors
[#3227](https://github.com/weaveworks/scope/pull/3227)
- Stop using dropped theme colors
[#3148](https://github.com/weaveworks/scope/pull/3148)
- Merge neutral theme colors
[#3146](https://github.com/weaveworks/scope/pull/3146)
- Slightly lightening background to match the rest of WeaveCloud
[#3206](https://github.com/weaveworks/scope/pull/3206)
- Sentence cased text everywhere
[#3166](https://github.com/weaveworks/scope/pull/3166)
- Show image tag more clearly in node details
[#3173](https://github.com/weaveworks/scope/pull/3173)
- Standardise border radius
[#3170](https://github.com/weaveworks/scope/pull/3170)
- Enforce theme font sizes
[#3167](https://github.com/weaveworks/scope/pull/3167)
- Use only z-index values from the theme
[#3159](https://github.com/weaveworks/scope/pull/3159)
Weave Cloud specific
As well as some bug-fixes, refactoring of places where the integration
of Scope into the hosted Weave Cloud UI complicated the code.
- Correct api.getFluxImages usage
[#3233](https://github.com/weaveworks/scope/pull/3233)
- Show deployments in Time Travel
[#3222](https://github.com/weaveworks/scope/pull/3222)
- Separate API endpoint namespace from URL path part
[#3221](https://github.com/weaveworks/scope/pull/3221)
- Fix scope report download URL in Weave Cloud
[#3213](https://github.com/weaveworks/scope/pull/3213)
- Use common TimestampTag component
[#3195](https://github.com/weaveworks/scope/pull/3195)
- Change URL resolution to accommodate Weave Cloud paths
[#3175](https://github.com/weaveworks/scope/pull/3175)
- Support rendering node details extras
[#3244](https://github.com/weaveworks/scope/pull/3244)
- Support TimeTravel injection
[#3239](https://github.com/weaveworks/scope/pull/3239)
## Release 1.9.0
Highlights:
- Change in behaviour of table data: Docker labels are now sent in
full, while Docker environment variables are not reported by default
- Plugins can now render http links and show controls on more objects
New plugin features:
- Render http links in tables
[#3105](https://github.com/weaveworks/scope/pull/3105)
- Support plugin controls in K8s Service, DaemonSet, StatefulSet, Cronjob.
[#3110](https://github.com/weaveworks/scope/pull/3110)
Bug fixes and minor improvements:
- Work around Ubuntu kernel crash
[#3141](https://github.com/weaveworks/scope/pull/3141)
- Stop truncating tables; disable reporting Docker env vars by default
[#3139](https://github.com/weaveworks/scope/pull/3139)
- Don't show Failed pods
[#3126](https://github.com/weaveworks/scope/pull/3126)
- Make scope start with Docker for Mac again.
[#3140](https://github.com/weaveworks/scope/pull/3140)
- Fix browser history when deep linking into node details with time context
[#3134](https://github.com/weaveworks/scope/pull/3134)
- Move to more consistent colour theme
[#3116](https://github.com/weaveworks/scope/pull/3116)
[#3124](https://github.com/weaveworks/scope/pull/3124)
[#3136](https://github.com/weaveworks/scope/pull/3136)
- Fix format string only used in debugging
[#3129](https://github.com/weaveworks/scope/pull/3129)
- Fix docs for OpenShift installation
[#3128](https://github.com/weaveworks/scope/pull/3128)
Performance:
- Use unsafe merge in joinResults.addChildAndChildren()
[#3143](https://github.com/weaveworks/scope/pull/3143)
- Use single-owner code path to accumulate children when rendering
[#3138](https://github.com/weaveworks/scope/pull/3138)
- Simplify Map.Render()
[#3135](https://github.com/weaveworks/scope/pull/3135)
- Let probe send smaller 'shortcut' reports to update the UI faster
[#3121](https://github.com/weaveworks/scope/pull/3121)
## Release 1.8.0
Highlights:
+7
View File
@@ -0,0 +1,7 @@
## Community Code of Conduct
Weaveworks follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported by contacting a Weaveworks project maintainer, or
Alexis Richardson alexis@weave.works.
+115
View File
@@ -0,0 +1,115 @@
# How to Contribute
Scope is [Apache 2.0 licensed](LICENSE) and accepts contributions via GitHub
pull requests. This document outlines some of the conventions on development
workflow, commit message formatting, contact points and other resources to make
it easier to get your contribution accepted.
We gratefully welcome improvements to documentation as well as to code.
# Certificate of Origin
By contributing to this project you agree to the Developer Certificate of
Origin (DCO). This document was created by the Linux Kernel community and is a
simple statement that you, as a contributor, have the legal right to make the
contribution. No action from you is required, but it's a good idea to see the
[DCO](DCO) file for details before you start contributing code to Scope.
# Email, Chat and Community Meetings
The project uses the the scope-community email list and Slack:
- Email: [scope-community](https://groups.google.com/forum/#!forum/scope-community)
- Chat: Join the [Weave community](https://weaveworks.github.io/community-slack/) Slack workspace and use the [#scope](https://weave-community.slack.com/messages/scope/) channel
When sending email, it's usually best to use the mailing list. The maintainers are usually quite busy and the mailing list will more easily find somebody who can reply quickly. You will also be potentially be helping others who had the same question.
We also meet regularly at the [Scope community meeting](https://docs.google.com/document/d/103_60TuEkfkhz_h2krrPJH8QOx-vRnPpbcCZqrddE1s/). Don't feel discouraged to attend the meeting due to not being a developer. Everybody is welcome!
## Getting Started
- Fork the repository on GitHub
- Read the [README](README.md) for getting started as a user and learn how/where to ask for help
- If you want to contribute as a developer, continue reading this document for further instructions
- Play with the project, submit bugs, submit pull requests!
## Contribution workflow
This is a rough outline of how to prepare a contribution:
- Create a topic branch from where you want to base your work (usually branched from master).
- Make commits of logical units.
- Make sure your commit messages are in the proper format (see below).
- Push your changes to a topic branch in your fork of the repository.
- If you changed code:
- add automated tests to cover your changes
- Submit a pull request to the original repository.
## How to build and run the project
```bash
make
./scope launch
```
## How to run the test suite
You can run the linting and unit tests by simply doing
```bash
make tests
```
There are integration tests for Scope, but unfortunately it's hard to set them up in forked repositories and the setup is not documented. Help is needed to improve this situation: https://github.com/weaveworks/scope/issues/2192
# Acceptance policy
These things will make a PR more likely to be accepted:
* a well-described requirement
* tests for new code
* tests for old code!
* new code and tests follow the conventions in old code and tests
* a good commit message (see below)
In general, we will merge a PR once two maintainers have endorsed it.
Trivial changes (e.g., corrections to spelling) may get waved through.
For substantial changes, more people may become involved, and you might get asked to resubmit the PR or divide the changes into more than one PR.
### Format of the Commit Message
We follow a rough convention for commit messages that is designed to answer two
questions: what changed and why. The subject line should feature the what and
the body of the commit should describe the why.
```
scripts: add the test-cluster command
this uses tmux to setup a test cluster that you can easily kill and
start for debugging.
Fixes #38
```
The format can be described more formally as follows:
```
<subsystem>: <what changed>
<BLANK LINE>
<why this change was made>
<BLANK LINE>
<footer>
```
The first line is the subject and should be no longer than 70 characters, the
second line is always blank, and other lines should be wrapped at 80 characters.
This allows the message to be easier to read on GitHub as well as in various
git tools.
## 3rd party plugins
So you've built a Scope plugin. Where should it live?
Until it matures, it should live in your own repo. You are encouraged to annouce your plugin at the [mailing list](https://groups.google.com/forum/#!forum/scope-community) and to demo it at a [community meetings](https://docs.google.com/document/d/103_60TuEkfkhz_h2krrPJH8QOx-vRnPpbcCZqrddE1s/).
If you have a good reason why the Scope maintainers should take custody of your
plugin, please open an issue so that it can potentially be promoted to the [Scope plugins](https://github.com/weaveworks-plugins/) organization.
+175
View File
@@ -0,0 +1,175 @@
./tools/integration/assert.sh is a copy of
https://github.com/lehmannro/assert.sh/blob/master/assert.sh
Since it was imported from its original source, it has only received
cosmetic modifications. As it is licensed under the LGPL-3, here's the
license text in its entirety:
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
+36
View File
@@ -0,0 +1,36 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
+58
View File
@@ -0,0 +1,58 @@
# Scope Governance
This document defines project governance for the project. This is (and probably will always be) a Work in Progress.
## Goals and Principles
Scope's community goals and principles:
1. Transition from a primarily Weaveworks project to a true community-driven project with autonomous governance. Weaveworks noticed interest from various actors and decided to nurture a community and see where it can lead the project.
2. Fill-in the needs of the community with a chop-wood-and-carry-water attitude: expect to give back before you take if you want to make an impact. Demands and suggestions from community members will be taken into account but actions and help will be more-highly appreciated.
## Code of Conduct
The Scope community abides by the CNCF [code of conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). Here is an excerpt:
_As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._
As a member of the Scope project, you represent the project and your fellow contributors.
We value our community tremendously and we'd like to keep cultivating a friendly and collaborative
environment for our contributors and users. We want everyone in the community to have
[positive experiences](https://www.cncf.io/blog/2016/12/14/diversity-scholarship-series-one-software-engineers-unexpected-cloudnativecon-kubecon-experience).
## Voting
The Scope project aims to employ "organization voting" to ensure no single organization can dominate the project. [Alfonso Acosta](https://github.com/2opremio) will take of the initial maintenance until enough voters join the community. Once the community reaches critical mass and sufficient maintainers are designed, the voting-based governance will start.
Individuals not associated with or employed by a company or organization are allowed one organization vote.
Each company or organization (regardless of the number of maintainers associated with or employed by that company/organization) receives one organization vote.
In other words, if two maintainers are employed by Company X, two by Company Y, two by Company Z, and one maintainer is an un-affiliated individual, a total of four "organization votes" are possible; one for X, one for Y, one for Z, and one for the un-affiliated individual.
Any maintainer from an organization may cast the vote for that organization.
For formal votes, a specific statement of what is being voted on should be added to the relevant github issue or PR, and a link to that issue or PR added to the maintainers meeting agenda document.
Maintainers should indicate their yes/no vote on that issue or PR, and after a suitable period of time, the votes will be tallied and the outcome noted.
## Changes in Maintainership
New maintainers are proposed by an existing maintainer and are elected by a 2/3 majority organization vote.
Maintainers can be removed by a 2/3 majority organization vote.
## Approving PRs
Non-specification-related PRs may be merged after receiving at least two organization votes.
## Github Project Administration
Maintainers will be given write access to the [weaveworks/scope](https://github.com/weaveworks/scope) GitHub repository.
## Changes in Governance
All changes in Governance require a 2/3 majority organization vote.
## Other Changes
Unless specified above, all other changes to the project require a 2/3 majority organization vote.
Additionally, any maintainer may request that any change require a 2/3 majority organization vote.
+1 -1
View File
@@ -176,7 +176,7 @@
END OF TERMS AND CONDITIONS
Copyright 2014-2017 Weaveworks Ltd.
Copyright 2014-2018 Weaveworks Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+4
View File
@@ -0,0 +1,4 @@
Alfonso Acosta <fons@syntacticsugar.consulting> (@2opremio)
Filip Barl <filip@weave.works> (@fbarl)
Bryan Boreham <bryan@weave.works> (@bboreham)
Satyam Zode <satyam.zode@openebs.io> (@satyamz)
+88 -34
View File
@@ -1,4 +1,4 @@
.PHONY: all deps static clean realclean client-lint client-test client-sync backend frontend shell lint ui-upload
.PHONY: all cri deps static clean realclean client-lint client-test client-sync backend frontend shell lint ui-upload
# If you can use Docker without being root, you can `make SUDO= <target>`
SUDO=$(shell docker info >/dev/null 2>&1 || echo "sudo -E")
@@ -11,6 +11,7 @@ SCOPE_UI_BUILD_UPTODATE=.scope_ui_build.uptodate
SCOPE_BACKEND_BUILD_IMAGE=$(DOCKERHUB_USER)/scope-backend-build
SCOPE_BACKEND_BUILD_UPTODATE=.scope_backend_build.uptodate
SCOPE_VERSION=$(shell git rev-parse --short HEAD)
GIT_REVISION=$(shell git rev-parse HEAD)
WEAVENET_VERSION=2.1.3
RUNSVINIT=vendor/runsvinit/runsvinit
CODECGEN_DIR=vendor/github.com/ugorji/go/codec/codecgen
@@ -44,6 +45,16 @@ IMAGE_TAG=$(shell ./tools/image-tag)
all: $(SCOPE_EXPORT)
update-cri:
curl https://raw.githubusercontent.com/kubernetes/kubernetes/master/pkg/kubelet/apis/cri/runtime/v1alpha2/api.proto > cri/runtime/api.proto
protoc-gen-gofast:
@go get -u -v github.com/gogo/protobuf/protoc-gen-gofast
# Use cri target to download latest cri proto files and regenerate CRI runtime files.
cri: update-cri protoc-gen-gofast
@cd $(GOPATH)/src;protoc --proto_path=$(GOPATH)/src --gofast_out=plugins=grpc:. github.com/weaveworks/scope/cri/runtime/api.proto
docker/weave:
curl -L https://github.com/weaveworks/weave/releases/download/v$(WEAVENET_VERSION)/weave -o docker/weave
chmod u+x docker/weave
@@ -56,7 +67,7 @@ docker/%: %
cp $* docker/
%.tar: docker/Dockerfile.%
$(SUDO) docker build -t $(DOCKERHUB_USER)/$* -f $< docker/
$(SUDO) docker build --build-arg=revision=$(GIT_REVISION) -t $(DOCKERHUB_USER)/$* -f $< docker/
$(SUDO) docker tag $(DOCKERHUB_USER)/$* $(DOCKERHUB_USER)/$*:$(IMAGE_TAG)
$(SUDO) docker save $(DOCKERHUB_USER)/$*:latest > $@
@@ -88,7 +99,7 @@ $(SCOPE_EXE) $(RUNSVINIT) lint tests shell prog/staticui/staticui.go prog/extern
else
$(SCOPE_EXE): $(SCOPE_BACKEND_BUILD_UPTODATE)
$(SCOPE_EXE):
time $(GO) build $(GO_BUILD_FLAGS) -o $@ ./$(@D)
@strings $@ | grep cgo_stub\\\.go >/dev/null || { \
rm $@; \
@@ -107,24 +118,24 @@ $(CODECGEN_EXE): $(CODECGEN_DIR)/*.go
mkdir -p $(@D)
$(GO_HOST) build $(GO_BUILD_FLAGS) -o $@ ./$(CODECGEN_DIR)
$(RUNSVINIT): $(SCOPE_BACKEND_BUILD_UPTODATE)
$(RUNSVINIT):
time $(GO) build $(GO_BUILD_FLAGS) -o $@ ./$(@D)
shell: $(SCOPE_BACKEND_BUILD_UPTODATE)
shell:
/bin/bash
tests: $(SCOPE_BACKEND_BUILD_UPTODATE) $(CODECGEN_TARGETS) prog/staticui/staticui.go prog/externalui/externalui.go
tests: $(CODECGEN_TARGETS) prog/staticui/staticui.go prog/externalui/externalui.go
./tools/test -no-go-get -tags $(GO_BUILD_TAGS)
lint: $(SCOPE_BACKEND_BUILD_UPTODATE)
lint:
./tools/lint
./tools/shell-lint tools
prog/staticui/staticui.go: $(SCOPE_BACKEND_BUILD_UPTODATE)
prog/staticui/staticui.go:
mkdir -p prog/staticui
esc -o $@ -pkg staticui -prefix client/build client/build
prog/externalui/externalui.go: $(SCOPE_BACKEND_BUILD_UPTODATE)
prog/externalui/externalui.go:
mkdir -p prog/externalui
esc -o $@ -pkg externalui -prefix client/build-external -include '\.html$$' client/build-external
@@ -132,51 +143,91 @@ endif
ifeq ($(BUILD_IN_CONTAINER),true)
client/build/index.html: $(shell find client/app -type f) $(SCOPE_UI_BUILD_UPTODATE)
SCOPE_UI_TOOLCHAIN=.cache/build_node_modules
SCOPE_UI_TOOLCHAIN_UPTODATE=$(SCOPE_UI_TOOLCHAIN)/.uptodate
$(SCOPE_UI_TOOLCHAIN_UPTODATE): client/yarn.lock $(SCOPE_UI_BUILD_UPTODATE)
mkdir -p $(SCOPE_UI_TOOLCHAIN) client/node_modules
if test "true" != "$(SCOPE_SKIP_UI_ASSETS)"; then \
$(SUDO) docker run $(RM) $(RUN_FLAGS) \
-v $(shell pwd)/.cache:/home/weave/scope/.cache \
-v $(shell pwd)/client:/home/weave/scope/client \
-v $(shell pwd)/$(SCOPE_UI_TOOLCHAIN):/home/weave/scope/client/node_modules \
-w /home/weave/scope/client \
$(SCOPE_UI_BUILD_IMAGE) yarn install; \
fi
touch $(SCOPE_UI_TOOLCHAIN_UPTODATE)
client/build/index.html: $(shell find client/app -type f) $(SCOPE_UI_TOOLCHAIN_UPTODATE)
mkdir -p client/build
if test "true" != "$(SCOPE_SKIP_UI_ASSETS)"; then \
$(SUDO) docker run $(RM) $(RUN_FLAGS) -v $(shell pwd)/client/app:/home/weave/app \
-v $(shell pwd)/client/build:/home/weave/build \
$(SUDO) docker run $(RM) $(RUN_FLAGS) \
-v $(shell pwd)/.cache:/home/weave/scope/.cache \
-v $(shell pwd)/client:/home/weave/scope/client \
-v $(shell pwd)/$(SCOPE_UI_TOOLCHAIN):/home/weave/scope/client/node_modules \
-w /home/weave/scope/client \
$(SCOPE_UI_BUILD_IMAGE) yarn run build; \
fi
client/build-external/index.html: $(shell find client/app -type f) $(SCOPE_UI_BUILD_UPTODATE)
client/build-external/index.html: $(shell find client/app -type f) $(SCOPE_UI_TOOLCHAIN_UPTODATE)
mkdir -p client/build-external
if test "true" != "$(SCOPE_SKIP_UI_ASSETS)"; then \
$(SUDO) docker run $(RM) $(RUN_FLAGS) -v $(shell pwd)/client/app:/home/weave/app \
-v $(shell pwd)/client/build-external:/home/weave/build-external \
$(SUDO) docker run $(RM) $(RUN_FLAGS) \
-v $(shell pwd)/.cache:/home/weave/scope/.cache \
-v $(shell pwd)/client:/home/weave/scope/client \
-v $(shell pwd)/$(SCOPE_UI_TOOLCHAIN):/home/weave/scope/client/node_modules \
-w /home/weave/scope/client \
$(SCOPE_UI_BUILD_IMAGE) yarn run build-external; \
fi
client-test: $(shell find client/app/scripts -type f) $(SCOPE_UI_BUILD_UPTODATE)
$(SUDO) docker run $(RM) $(RUN_FLAGS) -v $(shell pwd)/client/app:/home/weave/app \
-v $(shell pwd)/client/test:/home/weave/test \
client-test: $(shell find client/app/scripts -type f) $(SCOPE_UI_TOOLCHAIN_UPTODATE)
$(SUDO) docker run $(RM) $(RUN_FLAGS) \
-v $(shell pwd)/.cache:/home/weave/scope/.cache \
-v $(shell pwd)/client/client:/home/weave/scope/client \
-v $(shell pwd)/$(SCOPE_UI_TOOLCHAIN):/home/weave/scope/client/node_modules \
-w /home/weave/scope/client \
$(SCOPE_UI_BUILD_IMAGE) yarn test
client-lint: $(SCOPE_UI_BUILD_UPTODATE)
$(SUDO) docker run $(RM) $(RUN_FLAGS) -v $(shell pwd)/client/app:/home/weave/app \
-v $(shell pwd)/client/test:/home/weave/test \
client-lint: $(SCOPE_UI_TOOLCHAIN_UPTODATE)
$(SUDO) docker run $(RM) $(RUN_FLAGS) \
-v $(shell pwd)/.cache:/home/weave/scope/.cache \
-v $(shell pwd)/client:/home/weave/scope/client \
-v $(shell pwd)/$(SCOPE_UI_TOOLCHAIN):/home/weave/scope/client/node_modules \
-w /home/weave/scope/client \
$(SCOPE_UI_BUILD_IMAGE) yarn run lint
client-start: $(SCOPE_UI_BUILD_UPTODATE)
$(SUDO) docker run $(RM) $(RUN_FLAGS) --net=host -v $(shell pwd)/client/app:/home/weave/app \
-v $(shell pwd)/client/build:/home/weave/build -e WEBPACK_SERVER_HOST \
client-start: $(SCOPE_UI_TOOLCHAIN_UPTODATE)
$(SUDO) docker run $(RM) $(RUN_FLAGS) --net=host \
-v $(shell pwd)/.cache:/home/weave/scope/.cache \
-v $(shell pwd)/client:/home/weave/scope/client \
-v $(shell pwd)/$(SCOPE_UI_TOOLCHAIN):/home/weave/scope/client/node_modules \
-e WEBPACK_SERVER_HOST \
-w /home/weave/scope/client \
$(SCOPE_UI_BUILD_IMAGE) yarn start
tmp/weave-scope.tgz: $(shell find client/app -type f) $(SCOPE_UI_BUILD_UPTODATE)
client/bundle/weave-scope.tgz: $(shell find client/app -type f) $(SCOPE_UI_TOOLCHAIN_UPTODATE)
$(sudo) docker run $(RUN_FLAGS) \
-v $(shell pwd)/client/app:/home/weave/app \
-v $(shell pwd)/tmp:/home/weave/tmp \
$(SCOPE_UI_BUILD_IMAGE) \
yarn run bundle
-v $(shell pwd)/.cache:/home/weave/scope/.cache \
-v $(shell pwd)/client:/home/weave/scope/client \
-v $(shell pwd)/$(SCOPE_UI_TOOLCHAIN):/home/weave/scope/client/node_modules \
-v $(shell pwd)/tmp:/home/weave/tmp \
-w /home/weave/scope/client \
$(SCOPE_UI_BUILD_IMAGE) yarn run bundle
else
client/build/index.html:
SCOPE_UI_TOOLCHAIN=client/node_modules
SCOPE_UI_TOOLCHAIN_UPTODATE=$(SCOPE_UI_TOOLCHAIN)/.uptodate
$(SCOPE_UI_TOOLCHAIN_UPTODATE): client/yarn.lock
if test "true" = "$(SCOPE_SKIP_UI_ASSETS)"; then mkdir -p $(SCOPE_UI_TOOLCHAIN); else cd client && yarn install; fi
touch $(SCOPE_UI_TOOLCHAIN_UPTODATE)
client/build/index.html: $(SCOPE_UI_TOOLCHAIN_UPTODATE)
mkdir -p client/build
if test "true" != "$(SCOPE_SKIP_UI_ASSETS)"; then cd client && yarn run build; fi
client/build-external/index.html:
client/build-external/index.html: $(SCOPE_UI_TOOLCHAIN_UPTODATE)
mkdir -p client/build-external
if test "true" != "$(SCOPE_SKIP_UI_ASSETS)"; then cd client && yarn run build-external; fi
@@ -184,10 +235,12 @@ endif
$(SCOPE_UI_BUILD_UPTODATE): client/Dockerfile client/package.json client/webpack.local.config.js client/webpack.production.config.js client/server.js client/.eslintrc
$(SUDO) docker build -t $(SCOPE_UI_BUILD_IMAGE) client
$(SUDO) docker tag $(SCOPE_UI_BUILD_IMAGE) $(SCOPE_UI_BUILD_IMAGE):$(IMAGE_TAG)
touch $@
$(SCOPE_BACKEND_BUILD_UPTODATE): backend/*
$(SUDO) docker build -t $(SCOPE_BACKEND_BUILD_IMAGE) backend
$(SUDO) docker tag $(SCOPE_BACKEND_BUILD_IMAGE) $(SCOPE_BACKEND_BUILD_IMAGE):$(IMAGE_TAG)
touch $@
ui-upload: client/build-external/index.html
@@ -195,10 +248,10 @@ ui-upload: client/build-external/index.html
AWS_SECRET_ACCESS_KEY=$$UI_BUCKET_KEY_SECRET \
aws s3 cp client/build-external/ s3://static.weave.works/scope-ui/ --recursive --exclude '*.html'
ui-pkg-upload: tmp/weave-scope.tgz
ui-pkg-upload: client/bundle/weave-scope.tgz
AWS_ACCESS_KEY_ID=$$UI_BUCKET_KEY_ID \
AWS_SECRET_ACCESS_KEY=$$UI_BUCKET_KEY_SECRET \
aws s3 cp tmp/weave-scope.tgz s3://weaveworks-js-modules/weave-scope/$(shell echo $(SCOPE_VERSION))/weave-scope.tgz
aws s3 cp client/bundle/weave-scope.tgz s3://weaveworks-js-modules/weave-scope/$(shell echo $(SCOPE_VERSION))/weave-scope.tgz
# We don't rmi images here; rm'ing the .uptodate files is enough to
# get the build images rebuilt, and rm'ing the scope exe is enough to
@@ -207,7 +260,7 @@ ui-pkg-upload: tmp/weave-scope.tgz
# rmi'ng images is desirable sometimes. Invoke `realclean` for that.
clean:
$(GO) clean ./...
rm -rf $(SCOPE_EXPORT) $(SCOPE_UI_BUILD_UPTODATE) $(SCOPE_BACKEND_BUILD_UPTODATE) \
rm -rf $(SCOPE_EXPORT) $(SCOPE_UI_BUILD_UPTODATE) $(SCOPE_UI_TOOLCHAIN_UPTODATE) $(SCOPE_BACKEND_BUILD_UPTODATE) \
$(SCOPE_EXE) $(RUNSVINIT) prog/staticui/staticui.go prog/externalui/externalui.go client/build/*.js client/build-external/*.js docker/weave .pkg \
$(CODECGEN_TARGETS) $(CODECGEN_DIR)/bin
@@ -223,6 +276,7 @@ clean-codecgen:
#
# Doing this is important for release builds.
realclean: clean
rm -rf $(SCOPE_UI_TOOLCHAIN)
$(SUDO) docker rmi -f $(SCOPE_UI_BUILD_IMAGE) $(SCOPE_BACKEND_BUILD_IMAGE) \
$(DOCKERHUB_USER)/scope $(DOCKERHUB_USER)/cloud-agent \
$(DOCKERHUB_USER)/scope:$(IMAGE_TAG) $(DOCKERHUB_USER)/cloud-agent:$(IMAGE_TAG) \
+28 -17
View File
@@ -3,14 +3,13 @@
[![Circle CI](https://circleci.com/gh/weaveworks/scope/tree/master.svg?style=shield)](https://circleci.com/gh/weaveworks/scope/tree/master)
[![Coverage Status](https://coveralls.io/repos/weaveworks/scope/badge.svg)](https://coveralls.io/r/weaveworks/scope)
[![Go Report Card](https://goreportcard.com/badge/github.com/weaveworks/scope)](https://goreportcard.com/report/github.com/weaveworks/scope)
[![Slack Status](https://slack.weave.works/badge.svg)](https://slack.weave.works)
[![Docker Pulls](https://img.shields.io/docker/pulls/weaveworks/scope.svg?maxAge=604800)](https://hub.docker.com/r/weaveworks/scope/)
[![GoDoc](https://godoc.org/github.com/weaveworks/scope?status.svg)](https://godoc.org/github.com/weaveworks/scope)
Weave Scope automatically generates a map of your application, enabling you to
intuitively understand, monitor, and control your containerized, microservices based application.
intuitively understand, monitor, and control your containerized, microservices-based application.
### Understand your Docker containers in real-time
### Understand your Docker containers in real time
<img src="imgs/topology.png" width="200" alt="Map you architecture" align="right">
@@ -20,21 +19,21 @@ Choose an overview of your container infrastructure, or focus on a specific micr
<img src="imgs/selected.png" width="200" alt="Focus on a single container" align="right">
View contextual metrics, tags and metadata for your containers. Effortlessly navigate between processes inside your container to hosts your containers run on, arranged in expandable, sortable tables. Easily find the container using the most CPU or memory for a given host or service.
View contextual metrics, tags, and metadata for your containers. Effortlessly navigate between processes inside your container to hosts your containers run on, arranged in expandable, sortable tables. Easily find the container using the most CPU or memory for a given host or service.
### Interact with and manage containers
<img src="imgs/terminals.png" width="200" alt="Launch a command line." align="right">
Interact with your containers directly: pause, restart and stop containers. Launch a command line. All without leaving the scope browser window.
Interact with your containers directly: pause, restart, and stop containers. Launch a command line. All without leaving the scope browser window.
### Extend and customize via plugins
Add custom details or interactions for your hosts, containers and/or processes by creating Scope plugins; or just choose from some that others have already written at the Github [Weaveworks Scope Plugins](https://github.com/weaveworks-plugins/) organization.
Add custom details or interactions for your hosts, containers, and/or processes by creating Scope plugins. Or, just choose from some that others have already written at the GitHub [Weaveworks Scope Plugins](https://github.com/weaveworks-plugins/) organization.
## <a name="getting-started"></a>Getting started
## <a name="getting-started"></a>Getting Started
```
```console
sudo curl -L git.io/scope -o /usr/local/bin/scope
sudo chmod a+x /usr/local/bin/scope
scope launch
@@ -44,18 +43,30 @@ This script downloads and runs a recent Scope image from Docker Hub.
Now, open your web browser to **http://localhost:4040**. (If you're using
boot2docker, replace localhost with the output of `boot2docker ip`.)
For instructions on installing Scope on [Kubernetes](https://www.weave.works/docs/scope/latest/installing/#k8s), [DCOS](https://www.weave.works/docs/scope/latest/installing/#dcos) or [ECS](https://www.weave.works/docs/scope/latest/installing/#ecs), see [the docs](https://www.weave.works/docs/scope/latest/introducing/).
For instructions on installing Scope on [Kubernetes](https://www.weave.works/docs/scope/latest/installing/#k8s), [DCOS](https://www.weave.works/docs/scope/latest/installing/#dcos), or [ECS](https://www.weave.works/docs/scope/latest/installing/#ecs), see [the docs](https://www.weave.works/docs/scope/latest/introducing/).
## <a name="help"></a>Getting help
## <a name="help"></a>Getting Help
If you have any questions about, feedback for or problems with Scope:
We are a very friendly community and love questions, help and feedback.
- Read [the Weave Scope docs](https://www.weave.works/docs/scope/latest/introducing/).
- Invite yourself to the <a href="https://weaveworks.github.io/community-slack/" target="_blank"> #weave-community </a> slack channel.
- Ask a question on the <a href="https://weave-community.slack.com/messages/general/"> #weave-community</a> slack channel.
- Join the <a href="https://www.meetup.com/pro/Weave/"> Weave User Group </a> and get invited to online talks, hands-on training and meetups in your area.
- Send an email to <a href="mailto:weave-users@weave.works">weave-users@weave.works</a>
- <a href="https://github.com/weaveworks/scope/issues/new">File an issue.</a>
If you have any questions, feedback, or problems with Scope:
- Docs
- Read [the Weave Scope docs](https://www.weave.works/docs/scope/latest/introducing/)
- Check out the [frequently asked questions](/site/faq.md)
- Find out how to [contribute to Scope](CONTRIBUTING.md)
- Learn more about how the [Scope community operates](GOVERNANCE.md)
- Join the discussion
- Invite yourself to the <a href="https://slack.weave.works/" target="_blank">Weave community</a> Slack.
- Ask a question on the [#scope](https://weave-community.slack.com/messages/scope/) Slack channel.
- Send an email to [Scope community group](https://groups.google.com/forum/#!forum/scope-community).
- Meetings and events:
- Join the [Weave User Group](https://www.meetup.com/pro/Weave/) and get invited to online talks, hands-on training and meetups in your area.
- Join (and read up on) the regular [Scope community meetings](https://docs.google.com/document/d/103_60TuEkfkhz_h2krrPJH8QOx-vRnPpbcCZqrddE1s/edit).
- [File an issue](https://github.com/weaveworks/scope/issues/new).
Your feedback is always welcome!
### License
Scope is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text.
Find more details about the licenses of vendored code in [VENDORED_CODE.md](VENDORED_CODE.md).
+25
View File
@@ -0,0 +1,25 @@
# Use of vendored code in Weave Scope
Weave Scope is licensed under the [Apache 2.0 license](LICENSE).
Some vendored code is under different licenses though, all of them ship the
entire license text they are under.
- https://github.com/weaveworks/go-checkpoint
https://github.com/weaveworks/go-cleanhttp
https://github.com/certifi/gocertifi
can be found in the ./vendor/ directory, is under MPL-2.0.
- Pulled in by dependencies are
https://github.com/hashicorp/go-version (MPL-2.0)
https://github.com/hashicorp/golang-lru (MPL-2.0)
- One file pulled in by a dependency is under CDDL:
./vendor/github.com/howeyc/gopass/terminal_solaris.go
- The docs of a dependency that's pulled in by a dependency
are under CC-BY 4.0:
./vendor/github.com/docker/go-units/
[One file used in tests](COPYING.LGPL-3) is under LGPL-3, that's why we ship
the license text in this repository.
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"net/http"
"time"
"golang.org/x/net/context"
"context"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/report"
+51 -31
View File
@@ -1,6 +1,7 @@
package app
import (
"context"
"fmt"
"net/http"
"net/url"
@@ -9,9 +10,9 @@ import (
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/gorilla/mux"
"golang.org/x/net/context"
opentracing "github.com/opentracing/opentracing-go"
log "github.com/sirupsen/logrus"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/kubernetes"
@@ -43,8 +44,24 @@ var (
ID: "pseudo",
Default: "hide",
Options: []APITopologyOption{
{Value: "show", Label: "Show Unmanaged", filter: nil, filterPseudo: false},
{Value: "hide", Label: "Hide Unmanaged", filter: render.IsNotPseudo, filterPseudo: true},
{Value: "show", Label: "Show unmanaged", filter: nil, filterPseudo: false},
{Value: "hide", Label: "Hide unmanaged", filter: render.IsNotPseudo, filterPseudo: true},
},
}
storageFilter = APITopologyOptionGroup{
ID: "storage",
Default: "hide",
Options: []APITopologyOption{
{Value: "show", Label: "Show storage", filter: nil, filterPseudo: false},
{Value: "hide", Label: "Hide storage", filter: render.IsPodComponent, filterPseudo: false},
},
}
snapshotFilter = APITopologyOptionGroup{
ID: "snapshot",
Default: "hide",
Options: []APITopologyOption{
{Value: "show", Label: "Show snapshots", filter: nil, filterPseudo: false},
{Value: "hide", Label: "Hide snapshots", filter: render.IsNonSnapshotComponent, filterPseudo: false},
},
}
)
@@ -151,8 +168,8 @@ func MakeRegistry() *Registry {
Default: "application",
Options: []APITopologyOption{
{Value: "all", Label: "All", filter: nil, filterPseudo: false},
{Value: "system", Label: "System Containers", filter: render.IsSystem, filterPseudo: false},
{Value: "application", Label: "Application Containers", filter: render.IsApplication, filterPseudo: false}},
{Value: "system", Label: "System containers", filter: render.IsSystem, filterPseudo: false},
{Value: "application", Label: "Application containers", filter: render.IsApplication, filterPseudo: false}},
},
{
ID: "stopped",
@@ -167,8 +184,8 @@ func MakeRegistry() *Registry {
ID: "pseudo",
Default: "hide",
Options: []APITopologyOption{
{Value: "show", Label: "Show Uncontained", filter: nil, filterPseudo: false},
{Value: "hide", Label: "Hide Uncontained", filter: render.IsNotPseudo, filterPseudo: true},
{Value: "show", Label: "Show uncontained", filter: nil, filterPseudo: false},
{Value: "hide", Label: "Hide uncontained", filter: render.IsNotPseudo, filterPseudo: true},
},
},
}
@@ -178,8 +195,8 @@ func MakeRegistry() *Registry {
ID: "unconnected",
Default: "hide",
Options: []APITopologyOption{
{Value: "show", Label: "Show Unconnected", filter: nil, filterPseudo: false},
{Value: "hide", Label: "Hide Unconnected", filter: render.IsConnected, filterPseudo: false},
{Value: "show", Label: "Show unconnected", filter: nil, filterPseudo: false},
{Value: "hide", Label: "Hide unconnected", filter: render.IsConnected, filterPseudo: false},
},
},
}
@@ -189,7 +206,7 @@ func MakeRegistry() *Registry {
registry.Add(
APITopologyDesc{
id: processesID,
renderer: render.ProcessWithContainerNameRenderer,
renderer: render.ConnectedProcessRenderer,
Name: "Processes",
Rank: 1,
Options: unconnectedFilter,
@@ -229,14 +246,14 @@ func MakeRegistry() *Registry {
renderer: render.PodRenderer,
Name: "Pods",
Rank: 3,
Options: []APITopologyOptionGroup{unmanagedFilter},
Options: []APITopologyOptionGroup{snapshotFilter, storageFilter, unmanagedFilter},
HideIfEmpty: true,
},
APITopologyDesc{
id: kubeControllersID,
parent: podsID,
renderer: render.KubeControllerRenderer,
Name: "controllers",
Name: "Controllers",
Options: []APITopologyOptionGroup{unmanagedFilter},
HideIfEmpty: true,
},
@@ -244,7 +261,7 @@ func MakeRegistry() *Registry {
id: servicesID,
parent: podsID,
renderer: render.PodServiceRenderer,
Name: "services",
Name: "Services",
Options: []APITopologyOptionGroup{unmanagedFilter},
HideIfEmpty: true,
},
@@ -260,14 +277,14 @@ func MakeRegistry() *Registry {
id: ecsServicesID,
parent: ecsTasksID,
renderer: render.ECSServiceRenderer,
Name: "services",
Name: "Services",
Options: []APITopologyOptionGroup{unmanagedFilter},
HideIfEmpty: true,
},
APITopologyDesc{
id: swarmServicesID,
renderer: render.SwarmServiceRenderer,
Name: "services",
Name: "Services",
Rank: 3,
Options: []APITopologyOptionGroup{unmanagedFilter},
HideIfEmpty: true,
@@ -314,7 +331,7 @@ func (a byName) Less(i, j int) bool { return a[i].Name < a[j].Name }
// APITopologyOptionGroup describes a group of APITopologyOptions
type APITopologyOptionGroup struct {
ID string `json:"id"`
// Default value for the UI to adopt. NOT used as the default if the value is omitted, allowing "" as a distinct value.
// Default value for the option. Used if the value is omitted; not used if the value is ""
Default string `json:"defaultValue"`
Options []APITopologyOption `json:"options,omitempty"`
// SelectType describes how options can be picked. Currently defined values:
@@ -328,18 +345,14 @@ type APITopologyOptionGroup struct {
// Get the render filters to use for this option group, if any, or nil otherwise.
func (g APITopologyOptionGroup) filter(value string) render.FilterFunc {
selectType := g.SelectType
if selectType == "" {
selectType = "one"
}
var values []string
switch selectType {
case "one":
switch g.SelectType {
case "", "one":
values = []string{value}
case "union":
values = strings.Split(value, ",")
default:
log.Errorf("Invalid select type %s for option group %s, ignoring option", selectType, g.ID)
log.Errorf("Invalid select type %s for option group %s, ignoring option", g.SelectType, g.ID)
return nil
}
filters := []render.FilterFunc{}
@@ -466,32 +479,36 @@ func (r *Registry) makeTopologyList(rep Reporter) CtxHandlerFunc {
respondWith(w, http.StatusInternalServerError, err)
return
}
respondWith(w, http.StatusOK, r.renderTopologies(report, req))
respondWith(w, http.StatusOK, r.renderTopologies(ctx, report, req))
}
}
func (r *Registry) renderTopologies(rpt report.Report, req *http.Request) []APITopologyDesc {
func (r *Registry) renderTopologies(ctx context.Context, rpt report.Report, req *http.Request) []APITopologyDesc {
span, ctx := opentracing.StartSpanFromContext(ctx, "app.renderTopologies")
defer span.Finish()
topologies := []APITopologyDesc{}
req.ParseForm()
r.walk(func(desc APITopologyDesc) {
renderer, filter, _ := r.RendererForTopology(desc.id, req.Form, rpt)
desc.Stats = computeStats(rpt, renderer, filter)
desc.Stats = computeStats(ctx, rpt, renderer, filter)
for i, sub := range desc.SubTopologies {
renderer, filter, _ := r.RendererForTopology(sub.id, req.Form, rpt)
desc.SubTopologies[i].Stats = computeStats(rpt, renderer, filter)
desc.SubTopologies[i].Stats = computeStats(ctx, rpt, renderer, filter)
}
topologies = append(topologies, desc)
})
return updateFilters(rpt, topologies)
}
func computeStats(rpt report.Report, renderer render.Renderer, transformer render.Transformer) topologyStats {
func computeStats(ctx context.Context, rpt report.Report, renderer render.Renderer, transformer render.Transformer) topologyStats {
span, ctx := opentracing.StartSpanFromContext(ctx, "app.computeStats")
defer span.Finish()
var (
nodes int
realNodes int
edges int
)
r := render.Render(rpt, renderer, transformer)
r := render.Render(ctx, rpt, renderer, transformer)
for _, n := range r.Nodes {
nodes++
if n.Topology != render.Pseudo {
@@ -522,7 +539,10 @@ func (r *Registry) RendererForTopology(topologyID string, values url.Values, rpt
var filters []render.FilterFunc
for _, group := range topology.Options {
value := values.Get(group.ID)
value := group.Default
if vs := values[group.ID]; len(vs) > 0 {
value = vs[0]
}
if filter := group.filter(value); filter != nil {
filters = append(filters, filter)
}
+6 -4
View File
@@ -2,6 +2,7 @@ package app_test
import (
"bytes"
"context"
"net/http/httptest"
"net/url"
"testing"
@@ -51,7 +52,7 @@ func TestAPITopology(t *testing.T) {
}
// TODO: add ECS nodes in report fixture
if topology.Name == "Tasks" || topology.Name == "services" {
if topology.Name == "Tasks" || topology.Name == "Services" {
continue
}
@@ -118,7 +119,7 @@ func TestRendererForTopologyWithFiltering(t *testing.T) {
input.Container.Nodes[fixture.ClientContainerNodeID] = input.Container.Nodes[fixture.ClientContainerNodeID].WithLatests(map[string]string{
docker.LabelPrefix + "works.weave.role": "system",
})
have := utils.Prune(render.Render(input, renderer, filter).Nodes)
have := utils.Prune(render.Render(context.Background(), input, renderer, filter).Nodes)
want := utils.Prune(expected.RenderedContainers.Copy())
delete(want, fixture.ClientContainerNodeID)
delete(want, render.MakePseudoNodeID(render.UncontainedID, fixture.ServerHostID))
@@ -149,7 +150,7 @@ func TestRendererForTopologyNoFiltering(t *testing.T) {
input.Container.Nodes[fixture.ClientContainerNodeID] = input.Container.Nodes[fixture.ClientContainerNodeID].WithLatests(map[string]string{
docker.LabelPrefix + "works.weave.role": "system",
})
have := utils.Prune(render.Render(input, renderer, filter).Nodes)
have := utils.Prune(render.Render(context.Background(), input, renderer, filter).Nodes)
want := utils.Prune(expected.RenderedContainers.Copy())
delete(want, render.MakePseudoNodeID(render.UncontainedID, fixture.ServerHostID))
delete(want, render.OutgoingInternetID)
@@ -183,7 +184,8 @@ func getTestContainerLabelFilterTopologySummary(t *testing.T, exclude bool) (det
return nil, err
}
return detailed.Summaries(detailed.RenderContext{Report: fixture.Report}, render.Render(fixture.Report, renderer, filter).Nodes), nil
ctx := context.Background()
return detailed.Summaries(ctx, detailed.RenderContext{Report: fixture.Report}, render.Render(ctx, fixture.Report, renderer, filter).Nodes), nil
}
func TestAPITopologyAddsKubernetes(t *testing.T) {
+5 -5
View File
@@ -4,9 +4,9 @@ import (
"net/http"
"time"
log "github.com/Sirupsen/logrus"
"context"
"github.com/gorilla/mux"
"golang.org/x/net/context"
log "github.com/sirupsen/logrus"
"github.com/weaveworks/scope/common/xfer"
"github.com/weaveworks/scope/render"
@@ -42,7 +42,7 @@ type rendererHandler func(context.Context, render.Renderer, render.Transformer,
// Full topology.
func handleTopology(ctx context.Context, renderer render.Renderer, transformer render.Transformer, rc detailed.RenderContext, w http.ResponseWriter, r *http.Request) {
respondWith(w, http.StatusOK, APITopology{
Nodes: detailed.Summaries(rc, render.Render(rc.Report, renderer, transformer).Nodes),
Nodes: detailed.Summaries(ctx, rc, render.Render(ctx, rc.Report, renderer, transformer).Nodes),
})
}
@@ -58,7 +58,7 @@ func handleNode(ctx context.Context, renderer render.Renderer, transformer rende
// filtering, which gives us the node (if it exists at all), and
// then (2) applying the filter separately to that result. If the
// node is lost in the second step, we simply put it back.
nodes := renderer.Render(rc.Report)
nodes := renderer.Render(ctx, rc.Report)
node, ok := nodes.Nodes[nodeID]
if !ok {
http.NotFound(w, r)
@@ -145,7 +145,7 @@ func handleWebsocket(
log.Errorf("Error generating report: %v", err)
return
}
newTopo := detailed.Summaries(RenderContextForReporter(rep, re), render.Render(re, renderer, filter).Nodes)
newTopo := detailed.Summaries(ctx, RenderContextForReporter(rep, re), render.Render(ctx, re, renderer, filter).Nodes)
diff := detailed.TopoDiff(previousTopo, newTopo)
previousTopo = newTopo
+11 -5
View File
@@ -1,7 +1,9 @@
package app
import (
"context"
"flag"
"math/rand"
"net/http"
"net/url"
"os"
@@ -65,7 +67,10 @@ func BenchmarkReportUpgrade(b *testing.B) {
func BenchmarkReportMerge(b *testing.B) {
reports := upgradeReports(readReportFiles(b, *benchReportPath))
merger := NewSmartMerger()
rand.Shuffle(len(reports), func(i, j int) {
reports[i], reports[j] = reports[j], reports[i]
})
merger := NewFastMerger()
b.ResetTimer()
for i := 0; i < b.N; i++ {
merger.Merge(reports)
@@ -75,7 +80,7 @@ func BenchmarkReportMerge(b *testing.B) {
func getReport(b *testing.B) report.Report {
r := fixture.Report
if *benchReportPath != "" {
r = NewSmartMerger().Merge(upgradeReports(readReportFiles(b, *benchReportPath)))
r = NewFastMerger().Merge(upgradeReports(readReportFiles(b, *benchReportPath)))
}
return r
}
@@ -96,7 +101,7 @@ func renderForTopology(b *testing.B, topologyID string, report report.Report) re
if err != nil {
b.Fatal(err)
}
return render.Render(report, renderer, filter).Nodes
return render.Render(context.Background(), report, renderer, filter).Nodes
}
func benchmarkRenderTopology(b *testing.B, topologyID string) {
@@ -107,7 +112,7 @@ func benchmarkRenderTopology(b *testing.B, topologyID string) {
func BenchmarkRenderList(b *testing.B) {
benchmarkRender(b, func(report report.Report) {
topologyRegistry.renderTopologies(report, &http.Request{Form: url.Values{}})
topologyRegistry.renderTopologies(context.Background(), report, &http.Request{Form: url.Values{}})
})
}
@@ -136,12 +141,13 @@ func BenchmarkRenderProcessNames(b *testing.B) {
}
func benchmarkSummarizeTopology(b *testing.B, topologyID string) {
ctx := context.Background()
r := getReport(b)
rc := detailed.RenderContext{Report: r}
nodes := renderForTopology(b, topologyID, r)
b.ResetTimer()
for i := 0; i < b.N; i++ {
detailed.Summaries(rc, nodes)
detailed.Summaries(ctx, rc, nodes)
}
}
+3 -3
View File
@@ -9,7 +9,7 @@ import (
"sync"
"time"
"golang.org/x/net/context"
"context"
"github.com/weaveworks/common/mtime"
"github.com/weaveworks/scope/report"
@@ -107,7 +107,7 @@ func NewCollector(window time.Duration) Collector {
waitableCondition: waitableCondition{
waiters: map[chan struct{}]struct{}{},
},
merger: NewSmartMerger(),
merger: NewFastMerger(),
}
}
@@ -292,7 +292,7 @@ func NewFileCollector(path string, window time.Duration) (Collector, error) {
go replay(collector, timestamps, reports)
return collector, nil
}
return StaticCollector(NewSmartMerger().Merge(reports).Upgrade()), nil
return StaticCollector(NewFastMerger().Merge(reports).Upgrade()), nil
}
func timestampFromFilepath(path string) (time.Time, error) {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"testing"
"time"
"golang.org/x/net/context"
"context"
"github.com/weaveworks/common/mtime"
"github.com/weaveworks/common/test"
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"math/rand"
"sync"
"golang.org/x/net/context"
"context"
"github.com/weaveworks/scope/common/xfer"
)
+2 -2
View File
@@ -4,10 +4,10 @@ import (
"net/http"
"net/rpc"
log "github.com/Sirupsen/logrus"
"context"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"github.com/ugorji/go/codec"
"golang.org/x/net/context"
"github.com/weaveworks/scope/common/xfer"
)
+6 -36
View File
@@ -13,50 +13,20 @@ type Merger interface {
Merge([]report.Report) report.Report
}
type dumbMerger struct{}
type fastMerger struct{}
// MakeDumbMerger makes a Merger which merges together reports in the simplest possible way.
func MakeDumbMerger() Merger {
return dumbMerger{}
// NewFastMerger makes a Merger which merges together reports, mutating the one we are building up
func NewFastMerger() Merger {
return fastMerger{}
}
func (dumbMerger) Merge(reports []report.Report) report.Report {
func (fastMerger) Merge(reports []report.Report) report.Report {
rpt := report.MakeReport()
id := murmur3.New64()
for _, r := range reports {
rpt = rpt.Merge(r)
rpt.UnsafeMerge(r)
id.Write([]byte(r.ID))
}
rpt.ID = fmt.Sprintf("%x", id.Sum64())
return rpt
}
type smartMerger struct{}
// NewSmartMerger makes a Merger which merges reports in
// parallel. Speed up comes from the fact that a) most merges are
// between small reports, and b) we take advantage of available cores.
func NewSmartMerger() Merger {
return smartMerger{}
}
func (smartMerger) Merge(reports []report.Report) report.Report {
l := len(reports)
switch l {
case 0:
return report.MakeReport()
case 1:
return reports[0]
}
c := make(chan report.Report, l)
for _, r := range reports {
c <- r
}
for ; l > 1; l-- {
left, right := <-c, <-c
go func() {
c <- left.Merge(right)
}()
}
return <-c
}
+9 -9
View File
@@ -27,7 +27,7 @@ func TestMerger(t *testing.T) {
want.Endpoint.AddNode(report.MakeNode("bar"))
want.Endpoint.AddNode(report.MakeNode("baz"))
for _, merger := range []app.Merger{app.MakeDumbMerger(), app.NewSmartMerger()} {
for _, merger := range []app.Merger{app.NewFastMerger()} {
// Test the empty list case
if have := merger.Merge([]report.Report{}); !reflect.DeepEqual(have, report.MakeReport()) {
t.Errorf("Bad merge: %s", test.Diff(have, want))
@@ -44,12 +44,8 @@ func TestMerger(t *testing.T) {
}
}
func BenchmarkSmartMerger(b *testing.B) {
benchmarkMerger(b, app.NewSmartMerger())
}
func BenchmarkDumbMerger(b *testing.B) {
benchmarkMerger(b, app.MakeDumbMerger())
func BenchmarkFastMerger(b *testing.B) {
benchmarkMerger(b, app.NewFastMerger())
}
const numHosts = 15
@@ -67,13 +63,17 @@ func benchmarkMerger(b *testing.B, merger app.Merger) {
for i := 0; i < numHosts*5; i++ {
reports = append(reports, makeReport())
}
replacements := []report.Report{}
for i := 0; i < numHosts/3; i++ {
replacements = append(replacements, makeReport())
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
// replace 1/3 of hosts work of reports & merge them all
for i := 0; i < numHosts/3; i++ {
reports[rand.Intn(len(reports))] = makeReport()
for i := 0; i < len(replacements); i++ {
reports[rand.Intn(len(reports))] = replacements[i]
}
merger.Merge(reports)
+30 -29
View File
@@ -8,15 +8,17 @@ import (
"sync"
"time"
log "github.com/Sirupsen/logrus"
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/bluele/gcache"
"github.com/nats-io/nats"
opentracing "github.com/opentracing/opentracing-go"
otlog "github.com/opentracing/opentracing-go/log"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
log "github.com/sirupsen/logrus"
"github.com/weaveworks/common/instrument"
"github.com/weaveworks/scope/app"
@@ -104,17 +106,14 @@ type AWSCollectorConfig struct {
NatsHost string
MemcacheClient *MemcacheClient
Window time.Duration
MaxTopNodes int
}
type awsCollector struct {
userIDer UserIDer
cfg AWSCollectorConfig
db *dynamodb.DynamoDB
s3 *S3Store
tableName string
merger app.Merger
inProcess inProcessStore
memcache *MemcacheClient
window time.Duration
nats *nats.Conn
waitersLock sync.Mutex
@@ -150,14 +149,10 @@ func NewAWSCollector(config AWSCollectorConfig) (AWSCollector, error) {
// (window * report rate) * number of hosts per user * number of users
reportCacheSize := (int(config.Window.Seconds()) / 3) * 10 * 5
return &awsCollector{
cfg: config,
db: dynamodb.New(session.New(config.DynamoDBConfig)),
s3: config.S3Store,
userIDer: config.UserIDer,
tableName: config.DynamoTable,
merger: app.NewSmartMerger(),
merger: app.NewFastMerger(),
inProcess: newInProcessStore(reportCacheSize, config.Window),
memcache: config.MemcacheClient,
window: config.Window,
nats: nc,
waiters: map[watchKey]*nats.Subscription{},
}, nil
@@ -173,13 +168,13 @@ func (c *awsCollector) CreateTables() error {
return err
}
for _, s := range resp.TableNames {
if *s == c.tableName {
if *s == c.cfg.DynamoTable {
return nil
}
}
params := &dynamodb.CreateTableInput{
TableName: aws.String(c.tableName),
TableName: aws.String(c.cfg.DynamoTable),
AttributeDefinitions: []*dynamodb.AttributeDefinition{
{
AttributeName: aws.String(hourField),
@@ -210,7 +205,7 @@ func (c *awsCollector) CreateTables() error {
WriteCapacityUnits: aws.Int64(5),
},
}
log.Infof("Creating table %s", c.tableName)
log.Infof("Creating table %s", c.cfg.DynamoTable)
_, err = c.db.CreateTable(params)
return err
}
@@ -222,7 +217,7 @@ func (c *awsCollector) reportKeysInRange(ctx context.Context, userid string, row
err := instrument.TimeRequestHistogram(ctx, "DynamoDB.Query", dynamoRequestDuration, func(_ context.Context) error {
var err error
resp, err = c.db.Query(&dynamodb.QueryInput{
TableName: aws.String(c.tableName),
TableName: aws.String(c.cfg.DynamoTable),
KeyConditions: map[string]*dynamodb.Condition{
hourField: {
AttributeValueList: []*dynamodb.AttributeValue{
@@ -268,12 +263,12 @@ func (c *awsCollector) reportKeysInRange(ctx context.Context, userid string, row
func (c *awsCollector) getReportKeys(ctx context.Context, timestamp time.Time) ([]string, error) {
var (
end = timestamp
start = end.Add(-c.window)
start = end.Add(-c.cfg.Window)
rowStart = start.UnixNano() / time.Hour.Nanoseconds()
rowEnd = end.UnixNano() / time.Hour.Nanoseconds()
)
userid, err := c.userIDer(ctx)
userid, err := c.cfg.UserIDer(ctx)
if err != nil {
return nil, err
}
@@ -304,10 +299,10 @@ func (c *awsCollector) getReports(ctx context.Context, reportKeys []string) ([]r
missing := reportKeys
stores := []ReportStore{c.inProcess}
if c.memcache != nil {
stores = append(stores, c.memcache)
if c.cfg.MemcacheClient != nil {
stores = append(stores, c.cfg.MemcacheClient)
}
stores = append(stores, c.s3)
stores = append(stores, c.cfg.S3Store)
var reports []report.Report
for _, store := range stores {
@@ -320,6 +315,9 @@ func (c *awsCollector) getReports(ctx context.Context, reportKeys []string) ([]r
log.Warningf("Error fetching from cache: %v", err)
}
for key, report := range found {
if c.cfg.MaxTopNodes > 0 {
report = report.DropTopologiesOver(c.cfg.MaxTopNodes)
}
report = report.Upgrade()
c.inProcess.StoreReport(key, report)
reports = append(reports, report)
@@ -336,10 +334,13 @@ func (c *awsCollector) getReports(ctx context.Context, reportKeys []string) ([]r
}
func (c *awsCollector) Report(ctx context.Context, timestamp time.Time) (report.Report, error) {
span, ctx := opentracing.StartSpanFromContext(ctx, "awsCollector.Report")
defer span.Finish()
reportKeys, err := c.getReportKeys(ctx, timestamp)
if err != nil {
return report.MakeReport(), err
}
span.LogFields(otlog.Int("keys", len(reportKeys)), otlog.String("timestamp", timestamp.String()))
log.Debugf("Fetching %d reports to %v", len(reportKeys), timestamp)
reports, err := c.getReports(ctx, reportKeys)
if err != nil {
@@ -388,7 +389,7 @@ func (c *awsCollector) putItemInDynamo(rowKey, colKey, reportKey string) (*dynam
)
for {
resp, err = c.db.PutItem(&dynamodb.PutItemInput{
TableName: aws.String(c.tableName),
TableName: aws.String(c.cfg.DynamoTable),
Item: map[string]*dynamodb.AttributeValue{
hourField: {
S: aws.String(rowKey),
@@ -416,7 +417,7 @@ func (c *awsCollector) putItemInDynamo(rowKey, colKey, reportKey string) (*dynam
}
func (c *awsCollector) Add(ctx context.Context, rep report.Report, buf []byte) error {
userid, err := c.userIDer(ctx)
userid, err := c.cfg.UserIDer(ctx)
if err != nil {
return err
}
@@ -428,15 +429,15 @@ func (c *awsCollector) Add(ctx context.Context, rep report.Report, buf []byte) e
return err
}
reportSize, err := c.s3.StoreReportBytes(ctx, reportKey, buf)
reportSize, err := c.cfg.S3Store.StoreReportBytes(ctx, reportKey, buf)
if err != nil {
return err
}
reportSizeHistogram.Observe(float64(reportSize))
// third, put it in memcache
if c.memcache != nil {
_, err = c.memcache.StoreReportBytes(ctx, reportKey, buf)
if c.cfg.MemcacheClient != nil {
_, err = c.cfg.MemcacheClient.StoreReportBytes(ctx, reportKey, buf)
if err != nil {
// NOTE: We don't abort here because failing to store in memcache
// doesn't actually break anything else -- it's just an
@@ -476,7 +477,7 @@ func (c *awsCollector) Add(ctx context.Context, rep report.Report, buf []byte) e
}
func (c *awsCollector) WaitOn(ctx context.Context, waiter chan struct{}) {
userid, err := c.userIDer(ctx)
userid, err := c.cfg.UserIDer(ctx)
if err != nil {
log.Errorf("Error getting user id in WaitOn: %v", err)
return
@@ -517,7 +518,7 @@ func (c *awsCollector) WaitOn(ctx context.Context, waiter chan struct{}) {
}
func (c *awsCollector) UnWait(ctx context.Context, waiter chan struct{}) {
userid, err := c.userIDer(ctx)
userid, err := c.cfg.UserIDer(ctx)
if err != nil {
log.Errorf("Error getting user id in WaitOn: %v", err)
return
+2 -2
View File
@@ -7,9 +7,9 @@ import (
"strings"
"time"
log "github.com/Sirupsen/logrus"
"context"
log "github.com/sirupsen/logrus"
billing "github.com/weaveworks/billing-client"
"golang.org/x/net/context"
"github.com/weaveworks/scope/app"
"github.com/weaveworks/scope/report"
+1 -1
View File
@@ -6,8 +6,8 @@ import (
"fmt"
"time"
log "github.com/Sirupsen/logrus"
consul "github.com/hashicorp/consul/api"
log "github.com/sirupsen/logrus"
)
const (
+2 -2
View File
@@ -8,10 +8,10 @@ import (
"sync"
"time"
log "github.com/Sirupsen/logrus"
"context"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"golang.org/x/net/context"
log "github.com/sirupsen/logrus"
"github.com/weaveworks/common/mtime"
"github.com/weaveworks/scope/app"
@@ -9,7 +9,7 @@ import (
"sync"
"testing"
"golang.org/x/net/context"
"context"
"github.com/weaveworks/scope/app"
"github.com/weaveworks/scope/common/xfer"
+7 -2
View File
@@ -7,10 +7,12 @@ import (
"sync"
"time"
log "github.com/Sirupsen/logrus"
"context"
"github.com/bradfitz/gomemcache/memcache"
opentracing "github.com/opentracing/opentracing-go"
otlog "github.com/opentracing/opentracing-go/log"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
log "github.com/sirupsen/logrus"
"github.com/weaveworks/common/instrument"
"github.com/weaveworks/scope/report"
@@ -150,6 +152,8 @@ func memcacheStatusCode(err error) string {
// FetchReports gets reports from memcache.
func (c *MemcacheClient) FetchReports(ctx context.Context, keys []string) (map[string]report.Report, []string, error) {
span, ctx := opentracing.StartSpanFromContext(ctx, "Memcache.FetchReports")
defer span.Finish()
defer memcacheRequests.Add(float64(len(keys)))
var found map[string]*memcache.Item
err := instrument.TimeRequestHistogramStatus(ctx, "Memcache.GetMulti", memcacheRequestDuration, memcacheStatusCode, func(_ context.Context) error {
@@ -157,6 +161,7 @@ func (c *MemcacheClient) FetchReports(ctx context.Context, keys []string) (map[s
found, err = c.client.GetMulti(keys)
return err
})
span.LogFields(otlog.Int("keys", len(keys)), otlog.Int("hits", len(found)))
if err != nil {
return nil, keys, err
}
+1 -1
View File
@@ -3,11 +3,11 @@ package multitenant
import (
"bytes"
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
"github.com/weaveworks/common/instrument"
"github.com/weaveworks/scope/report"
+6 -5
View File
@@ -8,12 +8,12 @@ import (
"sync"
"time"
log "github.com/Sirupsen/logrus"
"context"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sqs"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
log "github.com/sirupsen/logrus"
"github.com/weaveworks/common/instrument"
"github.com/weaveworks/scope/app"
@@ -22,7 +22,6 @@ import (
var (
longPollTime = aws.Int64(10)
rpcTimeout = time.Minute
sqsRequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "scope",
Name: "sqs_request_duration_seconds",
@@ -45,6 +44,7 @@ type sqsControlRouter struct {
responseQueueURL *string
userIDer UserIDer
prefix string
rpcTimeout time.Duration
mtx sync.Mutex
responses map[string]chan xfer.Response
@@ -63,12 +63,13 @@ type sqsResponseMessage struct {
}
// NewSQSControlRouter the harbinger of death
func NewSQSControlRouter(config *aws.Config, userIDer UserIDer, prefix string) app.ControlRouter {
func NewSQSControlRouter(config *aws.Config, userIDer UserIDer, prefix string, rpcTimeout time.Duration) app.ControlRouter {
result := &sqsControlRouter{
service: sqs.New(session.New(config)),
responseQueueURL: nil,
userIDer: userIDer,
prefix: prefix,
rpcTimeout: rpcTimeout,
responses: map[string]chan xfer.Response{},
probeWorkers: map[int64]*probeWorker{},
}
@@ -257,7 +258,7 @@ func (cr *sqsControlRouter) Handle(ctx context.Context, probeID string, req xfer
select {
case response := <-waiter:
return response, nil
case <-time.After(rpcTimeout):
case <-time.After(cr.rpcTimeout):
return xfer.Response{}, fmt.Errorf("request timed out")
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"net/http"
"golang.org/x/net/context"
"context"
"github.com/weaveworks/scope/app"
)
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"sync"
"time"
log "github.com/Sirupsen/logrus"
"golang.org/x/net/context"
"context"
log "github.com/sirupsen/logrus"
"github.com/weaveworks/common/mtime"
"github.com/weaveworks/scope/common/xfer"
+2 -2
View File
@@ -3,9 +3,9 @@ package app
import (
"net/http"
log "github.com/Sirupsen/logrus"
"context"
"github.com/gorilla/mux"
"golang.org/x/net/context"
log "github.com/sirupsen/logrus"
"github.com/weaveworks/scope/common/xfer"
)
+1 -1
View File
@@ -11,9 +11,9 @@ import (
"testing"
"time"
"context"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"golang.org/x/net/context"
"github.com/weaveworks/common/mtime"
"github.com/weaveworks/scope/common/xfer"
+19 -23
View File
@@ -11,11 +11,11 @@ import (
"sync"
"time"
"github.com/PuerkitoBio/ghost/handlers"
log "github.com/Sirupsen/logrus"
"context"
"github.com/NYTimes/gziphandler"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"github.com/ugorji/go/codec"
"golang.org/x/net/context"
"github.com/weaveworks/scope/common/hostname"
"github.com/weaveworks/scope/common/xfer"
@@ -43,7 +43,7 @@ type CtxHandlerFunc func(context.Context, http.ResponseWriter, *http.Request)
func requestContextDecorator(f CtxHandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(context.Background(), RequestCtxKey, r)
ctx := context.WithValue(r.Context(), RequestCtxKey, r)
f(ctx, w, r)
}
}
@@ -86,32 +86,29 @@ func matchURL(r *http.Request, pattern string) (map[string]string, bool) {
return vars, true
}
func gzipHandler(h http.HandlerFunc) http.HandlerFunc {
return handlers.GZIPHandlerFunc(h, nil)
func gzipHandler(h http.HandlerFunc) http.Handler {
return gziphandler.GzipHandler(h)
}
// RegisterTopologyRoutes registers the various topology routes with a http mux.
func RegisterTopologyRoutes(router *mux.Router, r Reporter, capabilities map[string]bool) {
get := router.Methods("GET").Subrouter()
get.HandleFunc("/api",
get.Handle("/api",
gzipHandler(requestContextDecorator(apiHandler(r, capabilities))))
get.HandleFunc("/api/topology",
get.Handle("/api/topology",
gzipHandler(requestContextDecorator(topologyRegistry.makeTopologyList(r))))
get.
HandleFunc("/api/topology/{topology}",
gzipHandler(requestContextDecorator(topologyRegistry.captureRenderer(r, handleTopology)))).
get.Handle("/api/topology/{topology}",
gzipHandler(requestContextDecorator(topologyRegistry.captureRenderer(r, handleTopology)))).
Name("api_topology_topology")
get.
HandleFunc("/api/topology/{topology}/ws",
requestContextDecorator(captureReporter(r, handleWebsocket))). // NB not gzip!
get.Handle("/api/topology/{topology}/ws",
requestContextDecorator(captureReporter(r, handleWebsocket))). // NB not gzip!
Name("api_topology_topology_ws")
get.
MatcherFunc(URLMatcher("/api/topology/{topology}/{id}")).HandlerFunc(
get.MatcherFunc(URLMatcher("/api/topology/{topology}/{id}")).Handler(
gzipHandler(requestContextDecorator(topologyRegistry.captureRenderer(r, handleNode)))).
Name("api_topology_topology_id")
get.HandleFunc("/api/report",
get.Handle("/api/report",
gzipHandler(requestContextDecorator(makeRawReportHandler(r))))
get.HandleFunc("/api/probes",
get.Handle("/api/probes",
gzipHandler(requestContextDecorator(makeProbeHandler(r))))
}
@@ -121,13 +118,13 @@ func RegisterReportPostHandler(a Adder, router *mux.Router) {
post.HandleFunc("/api/report", requestContextDecorator(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
var (
rpt report.Report
buf bytes.Buffer
reader = io.TeeReader(r.Body, &buf)
buf = &bytes.Buffer{}
reader = io.TeeReader(r.Body, buf)
)
gzipped := strings.Contains(r.Header.Get("Content-Encoding"), "gzip")
if !gzipped {
reader = io.TeeReader(r.Body, gzip.NewWriter(&buf))
reader = io.TeeReader(r.Body, gzip.NewWriter(buf))
}
contentType := r.Header.Get("Content-Type")
@@ -150,8 +147,7 @@ func RegisterReportPostHandler(a Adder, router *mux.Router) {
// a.Add(..., buf) assumes buf is gzip'd msgpack
if !isMsgpack {
buf = bytes.Buffer{}
rpt.WriteBinary(&buf, gzip.DefaultCompression)
buf, _ = rpt.WriteBinary()
}
if err := a.Add(ctx, rpt, buf.Bytes()); err != nil {
+1 -1
View File
@@ -9,9 +9,9 @@ import (
"testing"
"time"
"context"
"github.com/gorilla/mux"
"github.com/ugorji/go/codec"
"golang.org/x/net/context"
"github.com/weaveworks/common/test"
"github.com/weaveworks/scope/app"
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/ugorji/go/codec"
log "github.com/Sirupsen/logrus"
log "github.com/sirupsen/logrus"
)
func respondWith(w http.ResponseWriter, code int, response interface{}) {
+53 -11
View File
@@ -1,23 +1,65 @@
FROM golang:1.9.2-stretch
FROM golang:1.10.2
ENV SCOPE_SKIP_UI_ASSETS true
RUN apt-get update && \
apt-get install -y libpcap-dev python-requests time file shellcheck git gcc-arm-linux-gnueabihf curl build-essential python-pip && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN set -eux; \
export arch_val="$(dpkg --print-architecture)"; \
apt-get update && \
if [ "$arch_val" = "amd64" ]; then \
apt-get install -y libpcap-dev time file shellcheck git gcc-arm-linux-gnueabihf curl build-essential python-pip; \
else \
apt-get install -y libpcap-dev time file shellcheck git curl build-essential python-pip; \
fi; \
\
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN go clean -i net && \
go install -tags netgo std && \
go install -race -tags netgo std
RUN curl -fsSL -o shfmt https://github.com/mvdan/sh/releases/download/v1.3.0/shfmt_v1.3.0_linux_amd64 && \
chmod +x shfmt && \
mv shfmt /usr/bin
RUN go get -tags netgo \
export arch_val="$(dpkg --print-architecture)"; \
if [ "$arch_val" != "ppc64el" ]; then \
go install -race -tags netgo std; \
fi; \
go get -tags netgo \
github.com/fzipp/gocyclo \
github.com/golang/lint/golint \
golang.org/x/lint/golint \
github.com/kisielk/errcheck \
github.com/fatih/hclfmt \
github.com/mjibson/esc \
github.com/client9/misspell/cmd/misspell && \
chmod a+wr --recursive /usr/local/go && \
rm -rf /go/pkg/ /go/src/
RUN pip install yapf==0.16.2 flake8==3.3.0
# Only install shfmt on amd64, as the version v1.3.0 isn't supported for ppc64le
# and the later version of shfmt doesn't work with the application well
RUN export arch_val="$(dpkg --print-architecture)"; \
if [ "$arch_val" = "amd64" ]; then \
curl -fsSL -o shfmt https://github.com/mvdan/sh/releases/download/v1.3.0/shfmt_v1.3.0_linux_amd64 && \
chmod +x shfmt && \
mv shfmt /usr/bin; \
fi;
RUN pip install yapf==0.16.2 flake8==3.3.0 requests==2.19.1
# Install Docker (client only)
ENV DOCKERVERSION=17.09.1-ce
RUN export arch_val="$(dpkg --print-architecture)"; \
if [ "$arch_val" = "arm64" ]; then \
curl -fsSLO https://download.docker.com/linux/static/stable/aarch64/docker-${DOCKERVERSION}.tgz; \
elif [ "$arch_val" = "amd64" ]; then \
curl -fsSLO https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKERVERSION}.tgz; \
elif [ "$arch_val" = "ppc64el" ]; then \
curl -fsSLO https://download.docker.com/linux/static/stable/ppc64le/docker-${DOCKERVERSION}.tgz; \
else \
echo "No Docker client found for architecture $(arch_val)." && \
exit 1; \
fi; \
tar xzvf docker-${DOCKERVERSION}.tgz --strip 1 -C /usr/local/bin docker/docker && \
rm docker-${DOCKERVERSION}.tgz;
COPY build.sh /
ENTRYPOINT ["/build.sh"]
ARG revision
LABEL maintainer="Weaveworks <help@weave.works>" \
org.opencontainers.image.title="backend" \
org.opencontainers.image.source="https://github.com/weaveworks/scope/tree/master/backend" \
org.opencontainers.image.revision="${revision}" \
org.opencontainers.image.vendor="Weaveworks"
-117
View File
@@ -1,117 +0,0 @@
general:
branches:
ignore:
- gh-pages
machine:
pre:
- curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0
services:
- docker
environment:
GOPATH: /home/ubuntu
SRCDIR: /home/ubuntu/src/github.com/weaveworks/scope
PATH: $PATH:$HOME/.local/bin
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
SCOPE_UI_BUILD: $HOME/docker/scope_ui_build.tar
IMAGES: scope cloud-agent
dependencies:
pre:
- pip install --upgrade requests
cache_directories:
- "~/docker"
override:
- |
sudo apt-get update &&
sudo apt-get install jq pv &&
sudo chmod a+wr --recursive /usr/local/go/pkg &&
sudo chown ubuntu:ubuntu "$HOME/.bashrc.backup"
(curl https://sdk.cloud.google.com | bash) &&
(test -z "$SECRET_PASSWORD" || bin/setup-circleci-secrets "$SECRET_PASSWORD") &&
make deps &&
mkdir -p $(dirname $SRCDIR) &&
cp -r $(pwd)/ $SRCDIR
- "cd $SRCDIR/client; ../tools/rebuild-image weaveworks/scope-ui-build . Dockerfile package.json webpack.production.config.js .eslintrc .babelrc && touch $SRCDIR/.scope_ui_build.uptodate"
- "cd $SRCDIR/backend; ../tools/rebuild-image weaveworks/scope-backend-build . Dockerfile build.sh && touch $SRCDIR/.scope_backend_build.uptodate"
- test -z "$SECRET_PASSWORD" || (cd $SRCDIR/integration; ./gce.sh make_template):
parallel: false
- sudo apt-get update && sudo apt-get install python-pip && sudo pip install awscli
test:
override:
- cd $SRCDIR; make RM= lint:
parallel: true
- cd $SRCDIR; COVERDIR=./coverage make RM= CODECGEN_UID=23 tests:
parallel: true
- cd $SRCDIR; make RM= client-test static:
parallel: true
- cd $SRCDIR; make RM= client-lint static:
parallel: true
- cd $SRCDIR; rm -f prog/scope; if [ "$CIRCLE_NODE_INDEX" = "0" ]; then GOARCH=arm make GO_BUILD_INSTALL_DEPS= RM= prog/scope; else GOOS=darwin make GO_BUILD_INSTALL_DEPS= RM= prog/scope; fi:
parallel: true
- cd $SRCDIR; rm -f prog/scope; make RM=:
parallel: true
- cd $SRCDIR/extras; ./build_on_circle.sh:
parallel: true
- "test -z \"$SECRET_PASSWORD\" || (cd $SRCDIR/integration; ./gce.sh setup && eval $(./gce.sh hosts); ./setup.sh)":
parallel: true
- test -z "$SECRET_PASSWORD" || (cd $SRCDIR/integration; eval $(./gce.sh hosts); ./run_all.sh):
parallel: true
timeout: 300
post:
- test -z "$SECRET_PASSWORD" || (cd $SRCDIR/integration; ./gce.sh destroy):
parallel: true
- test "$CIRCLE_NODE_INDEX" != "0" || (cd $SRCDIR; ./tools/cover/gather_coverage.sh ./coverage $SRCDIR/coverage):
parallel: true
- test "$CIRCLE_NODE_INDEX" != "0" || (goveralls -repotoken $COVERALLS_REPO_TOKEN -coverprofile=$SRCDIR/profile.cov -service=circleci || true):
parallel: true
- test "$CIRCLE_NODE_INDEX" != "0" || (cd $SRCDIR; cp */*.codecgen.go $CIRCLE_ARTIFACTS):
parallel: true
deployment:
hub:
branch: master
commands:
- |
test -z "${DOCKER_USER}" || (
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS &&
(test "${DOCKER_ORGANIZATION:-$DOCKER_USER}" = "weaveworks" || (
for IMAGE in $IMAGES; do
docker tag weaveworks/$IMAGE:latest ${DOCKER_ORGANIZATION:-$DOCKER_USER}/$IMAGE:latest &&
docker tag weaveworks/$IMAGE:$(./tools/image-tag) ${DOCKER_ORGANIZATION:-$DOCKER_USER}/$IMAGE:$(./tools/image-tag)
done
)) &&
for IMAGE in $IMAGES; do
docker push ${DOCKER_ORGANIZATION:-$DOCKER_USER}/$IMAGE &&
docker push ${DOCKER_ORGANIZATION:-$DOCKER_USER}/$IMAGE:$(./tools/image-tag)
done
)
- |
test -z "${QUAY_USER}" || (
docker login -e '.' -u "$QUAY_USER" -p "$QUAY_PASSWORD" quay.io &&
docker tag weaveworks/scope:$(./tools/image-tag) "quay.io/${QUAY_ORGANIZATION}/scope:$(./tools/image-tag)" &&
docker push "quay.io/${QUAY_ORGANIZATION}/scope:$(./tools/image-tag)"
)
- test -z "${UI_BUCKET_KEY_ID}" || (cd $SRCDIR && make ui-upload && make ui-pkg-upload)
hub-dev:
branch: /^((?!master).)*$/ # not the master branch
commands:
- >
test -z "${DEPLOY_BRANCH}" || test -z "${DOCKER_USER}" || (
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS &&
docker tag weaveworks/scope:latest ${DOCKER_ORGANIZATION:-$DOCKER_USER}/scope:${CIRCLE_BRANCH//\//-} &&
docker push ${DOCKER_ORGANIZATION:-$DOCKER_USER}/scope:${CIRCLE_BRANCH//\//-}
)
# release:
# branch: /release-[0-9]+\.[0-9]+/
# owner: weaveworks
# commands:
# - go get github.com/weaveworks/wordepress && cd /home/ubuntu/src/github.com/weaveworks/wordepress && git checkout v1.0.0 && cd cmd/wordepress && go get
# - cd $SRCDIR; PRODUCT=scope tools/publish-site "$WP_LIVE_URL" "$WP_LIVE_USER" "$WP_LIVE_PASSWORD"
# issues:
# branch: /.*/
# owner: weaveworks
# commands:
# - go get github.com/weaveworks/wordepress && cd /home/ubuntu/src/github.com/weaveworks/wordepress && git checkout v1.0.0 && cd cmd/wordepress && go get
# - cd $SRCDIR; PRODUCT=scope tools/publish-site "$WP_DEV_URL" "$WP_DEV_USER" "$WP_DEV_PASSWORD"
+1
View File
@@ -1,5 +1,6 @@
node_modules
build/
build-external/
coverage/
test/*png
weave-scope.tgz
+12
View File
@@ -5,3 +5,15 @@ files:
rules:
no-important: 1
no-color-literals: 2
variable-for-property:
- 2
-
properties:
- 'border-radius'
- 'border-top-left-radius'
- 'border-top-right-radius'
- 'border-bottom-left-radius'
- 'border-bottom-right-radius'
- 'font-family'
- 'font-size'
- 'z-index'
+11
View File
@@ -4,6 +4,7 @@
"stylelint-config-styled-components",
"stylelint-config-recommended",
],
"plugins": ["stylelint-declaration-use-variable"],
"rules": {
"block-no-empty": null,
"color-named": "never",
@@ -15,5 +16,15 @@
"ignoreProperties": ["tab-size", "hyphens"],
}],
"selector-type-no-unknown": null,
"sh-waqar/declaration-use-variable": [[
"border-radius",
"border-top-left-radius",
"border-top-right-radius",
"border-bottom-left-radius",
"border-bottom-right-radius",
"font-family",
"font-size",
"z-index"
]],
},
}
+14 -6
View File
@@ -1,6 +1,14 @@
FROM node:8.4.0
WORKDIR /home/weave
COPY package.json yarn.lock /home/weave/
ENV NPM_CONFIG_LOGLEVEL=warn NPM_CONFIG_PROGRESS=false
RUN yarn --pure-lockfile
COPY webpack-common.js webpack.local.config.js webpack.production.config.js server.js .babelrc .eslintrc .eslintignore .stylelintrc .sass-lint.yml /home/weave/
# Changes to this file will not take effect in CI
# until the image version in the CI config is updated. See
# https://github.com/weaveworks/scope/blob/master/.circleci/config.yml#L11
FROM node:8.11
ENV NPM_CONFIG_LOGLEVEL=warn
ENV NPM_CONFIG_PROGRESS=false
ENV XDG_CACHE_HOME=/home/weave/scope/.cache
ARG revision
LABEL maintainer="Weaveworks <help@weave.works>" \
org.opencontainers.image.title="scope-ui-build" \
org.opencontainers.image.source="https://github.com/weaveworks/scope" \
org.opencontainers.image.revision="${revision}" \
org.opencontainers.image.vendor="Weaveworks"
Binary file not shown.
Binary file not shown.
+53 -137
View File
@@ -1,10 +1,9 @@
import debug from 'debug';
import { find } from 'lodash';
import { fromJS } from 'immutable';
import ActionTypes from '../constants/action-types';
import { saveGraph } from '../utils/file-utils';
import { updateRoute } from '../utils/router-utils';
import { clearStoredViewState, updateRoute } from '../utils/router-utils';
import {
doControlRequest,
getAllNodes,
@@ -16,7 +15,6 @@ import {
teardownWebsockets,
getNodes,
} from '../utils/web-api-utils';
import { storageSet } from '../utils/storage-utils';
import { loadTheme } from '../utils/contrast-utils';
import { isPausedSelector } from '../selectors/time-travel';
import {
@@ -173,23 +171,29 @@ export function pinPreviousMetric() {
};
}
export function pinSearch() {
export function updateSearch(searchQuery = '', pinnedSearches = []) {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.PIN_SEARCH,
query: getState().get('searchQuery'),
type: ActionTypes.UPDATE_SEARCH,
pinnedSearches,
searchQuery,
});
updateRoute(getState);
};
}
export function unpinSearch(query) {
export function focusSearch() {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.UNPIN_SEARCH,
query
});
updateRoute(getState);
dispatch({ type: ActionTypes.FOCUS_SEARCH });
// update nodes cache to allow search across all topologies,
// wait a second until animation is over
// NOTE: This will cause matching recalculation (and rerendering)
// of all the nodes in the topology, instead applying it only on
// the nodes delta. The solution would be to implement deeper
// search selectors with per-node caching instead of per-topology.
setTimeout(() => {
getAllNodes(getState(), dispatch);
}, 1200);
};
}
@@ -234,10 +238,10 @@ export function clickCloseDetails(nodeId) {
};
}
export function clickCloseTerminal(pipeId) {
export function closeTerminal(pipeId) {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.CLICK_CLOSE_TERMINAL,
type: ActionTypes.CLOSE_TERMINAL,
pipeId
});
updateRoute(getState);
@@ -268,16 +272,6 @@ export function clickForceRelayout() {
};
}
export function doSearch(searchQuery) {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.DO_SEARCH,
searchQuery
});
updateRoute(getState);
};
}
export function setViewportDimensions(width, height) {
return (dispatch) => {
dispatch({ type: ActionTypes.SET_VIEWPORT_DIMENSIONS, width, height });
@@ -339,15 +333,14 @@ export function clickNode(nodeId, label, origin, topologyId = null) {
export function pauseTimeAtNow() {
return (dispatch, getState) => {
const getScopeState = () => getState().scope || getState();
dispatch({
type: ActionTypes.PAUSE_TIME_AT_NOW
});
updateRoute(getScopeState);
if (!getScopeState().get('nodesLoaded')) {
getNodes(getScopeState, dispatch);
if (isResourceViewModeSelector(getScopeState())) {
getResourceViewNodesSnapshot(getScopeState(), dispatch);
updateRoute(getState);
if (!getState().get('nodesLoaded')) {
getNodes(getState, dispatch);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
}
};
@@ -453,55 +446,16 @@ export function enterNode(nodeId) {
};
}
export function focusSearch() {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.FOCUS_SEARCH });
// update nodes cache to allow search across all topologies,
// wait a second until animation is over
// NOTE: This will cause matching recalculation (and rerendering)
// of all the nodes in the topology, instead applying it only on
// the nodes delta. The solution would be to implement deeper
// search selectors with per-node caching instead of per-topology.
setTimeout(() => {
getAllNodes(getState(), dispatch);
}, 1200);
};
}
export function hitBackspace() {
return (dispatch, getState) => {
const state = getState();
// remove last pinned query if search query is empty
if (state.get('searchFocused') && !state.get('searchQuery')) {
const query = state.get('pinnedSearches').last();
if (query) {
dispatch({
type: ActionTypes.UNPIN_SEARCH,
query
});
updateRoute(getState);
}
}
};
}
export function hitEsc() {
return (dispatch, getState) => {
const state = getState();
const controlPipe = state.get('controlPipes').last();
if (controlPipe && controlPipe.get('status') === 'PIPE_DELETED') {
dispatch({
type: ActionTypes.CLICK_CLOSE_TERMINAL,
type: ActionTypes.CLOSE_TERMINAL,
pipeId: controlPipe.get('id')
});
updateRoute(getState);
// Don't deselect node on ESC if there is a controlPipe (keep terminal open)
} else if (state.get('searchFocused')) {
if (state.get('searchQuery')) {
dispatch(doSearch(''));
} else {
dispatch(blurSearch());
}
} else if (state.get('showingHelp')) {
dispatch(hideHelp());
} else if (state.get('nodeDetails').last() && !controlPipe) {
@@ -550,8 +504,7 @@ export function receiveNodeDetails(details, requestTimestamp) {
export function receiveNodesDelta(delta) {
return (dispatch, getState) => {
const getScopeState = () => getState().scope || getState();
if (!isPausedSelector(getScopeState())) {
if (!isPausedSelector(getState())) {
// Allow css-animation to run smoothly by scheduling it to run on the
// next tick after any potentially expensive canvas re-draws have been
// completed.
@@ -561,7 +514,7 @@ export function receiveNodesDelta(delta) {
// only when the first batch of nodes delta has been received. We
// do that because we want to keep the previous state blurred instead
// of transitioning over an empty state like when switching topologies.
if (getScopeState().get('timeTravelTransitioning')) {
if (getState().get('timeTravelTransitioning')) {
dispatch({ type: ActionTypes.FINISH_TIME_TRAVEL_TRANSITION });
}
@@ -578,37 +531,17 @@ export function receiveNodesDelta(delta) {
export function resumeTime() {
return (dispatch, getState) => {
const getScopeState = () => getState().scope || getState();
if (isPausedSelector(getScopeState())) {
if (isPausedSelector(getState())) {
dispatch({
type: ActionTypes.RESUME_TIME
});
updateRoute(getScopeState);
updateRoute(getState);
// After unpausing, all of the following calls will re-activate polling.
getTopologies(getScopeState, dispatch);
getNodes(getScopeState, dispatch, true);
if (isResourceViewModeSelector(getScopeState())) {
getResourceViewNodesSnapshot(getScopeState(), dispatch);
}
}
};
}
export function startTimeTravel(timestamp = null) {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.START_TIME_TRAVEL,
timestamp,
});
updateRoute(getState);
if (!getState().get('nodesLoaded')) {
getNodes(getState, dispatch);
getTopologies(getState, dispatch);
getNodes(getState, dispatch, true);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
} else {
// Get most recent details before freezing the state.
getNodeDetails(getState, dispatch);
}
};
}
@@ -622,16 +555,20 @@ export function receiveNodes(nodes) {
export function jumpToTime(timestamp) {
return (dispatch, getState) => {
const getScopeState = () => getState().scope || getState();
dispatch({
type: ActionTypes.JUMP_TO_TIME,
timestamp,
});
updateRoute(getScopeState);
getNodes(getScopeState, dispatch);
getTopologies(getScopeState, dispatch);
if (isResourceViewModeSelector(getScopeState())) {
getResourceViewNodesSnapshot(getScopeState(), dispatch);
updateRoute(getState);
getTopologies(getState, dispatch);
if (!getState().get('nodesLoaded')) {
getNodes(getState, dispatch);
if (isResourceViewModeSelector(getState())) {
getResourceViewNodesSnapshot(getState(), dispatch);
}
} else {
// Get most recent details before freezing the state.
getNodeDetails(getState, dispatch);
}
};
}
@@ -646,18 +583,14 @@ export function receiveNodesForTopology(nodes, topologyId) {
export function receiveTopologies(topologies) {
return (dispatch, getState) => {
const getScopeState = () => getState().scope || getState();
const firstLoad = !getScopeState().get('topologiesLoaded');
const firstLoad = !getState().get('topologiesLoaded');
dispatch({
type: ActionTypes.RECEIVE_TOPOLOGIES,
topologies
});
getNodes(getScopeState, dispatch);
getNodes(getState, dispatch);
// Populate search matches on first load
const state = getScopeState();
if (firstLoad && state.get('searchQuery')) {
dispatch(focusSearch());
}
const state = getState();
// Fetch all the relevant nodes once on first load
if (firstLoad && isResourceViewModeSelector(state)) {
getResourceViewNodesSnapshot(state, dispatch);
@@ -687,7 +620,7 @@ export function receiveApiDetails(apiDetails) {
// we have no prior info on whether time travel would be available.
if (isFirstTime && pausedAt) {
if (apiDetails.capabilities && apiDetails.capabilities.historic_reports) {
dispatch(startTimeTravel(pausedAt));
dispatch(jumpToTime(pausedAt));
} else {
dispatch(pauseTimeAtNow());
}
@@ -796,7 +729,7 @@ export function route(urlState) {
if (!urlState.pausedAt) {
dispatch(resumeTime());
} else {
dispatch(startTimeTravel(urlState.pausedAt));
dispatch(jumpToTime(urlState.pausedAt));
}
// update all request workers with new options
getTopologies(getState, dispatch);
@@ -814,7 +747,7 @@ export function route(urlState) {
export function resetLocalViewState() {
return (dispatch) => {
dispatch({type: ActionTypes.RESET_LOCAL_VIEW_STATE});
storageSet('scopeViewState', '');
clearStoredViewState();
// eslint-disable-next-line prefer-destructuring
window.location.href = window.location.href.split('#')[0];
};
@@ -846,33 +779,16 @@ export function shutdown() {
};
}
export function getImagesForService(orgId, serviceId) {
return (dispatch, getState, { api }) => {
dispatch({
type: ActionTypes.REQUEST_SERVICE_IMAGES,
serviceId
});
// Use the fluxv2 api
api.getFluxImages(orgId, serviceId, 2)
.then((services) => {
dispatch({
type: ActionTypes.RECEIVE_SERVICE_IMAGES,
service: find(services, s => s.ID === serviceId),
serviceId
});
}, ({ errors }) => {
dispatch({
type: ActionTypes.RECEIVE_SERVICE_IMAGES,
errors
});
});
};
}
export function setMonitorState(monitor) {
return {
type: ActionTypes.MONITOR_STATE,
monitor
};
}
export function setStoreViewState(storeViewState) {
return {
type: ActionTypes.SET_STORE_VIEW_STATE,
storeViewState
};
}
+25 -1
View File
@@ -5,6 +5,26 @@ import classNames from 'classnames';
import { enterEdge, leaveEdge } from '../actions/app-actions';
import { encodeIdAttribute, decodeIdAttribute } from '../utils/dom-utils';
function isStorageComponent(id) {
const storageComponents = ['<persistent_volume>', '<storage_class>', '<persistent_volume_claim>', '<volume_snapshot>', '<volume_snapshot_data>'];
return storageComponents.includes(id);
}
// getAdjacencyClass takes id which contains information about edge as a topology
// of parent and child node.
// For example: id is of form "nodeA;<storage_class>---nodeB;<persistent_volume_claim>"
function getAdjacencyClass(id) {
const topologyId = id.split('---');
const fromNode = topologyId[0].split(';');
const toNode = topologyId[1].split(';');
if (fromNode[1] !== undefined && toNode[1] !== undefined) {
if (isStorageComponent(fromNode[1]) || isStorageComponent(toNode[1])) {
return 'link-storage';
}
}
return 'link-none';
}
class Edge extends React.Component {
constructor(props, context) {
super(props, context);
@@ -18,7 +38,6 @@ class Edge extends React.Component {
} = this.props;
const shouldRenderMarker = (focused || highlighted) && (source !== target);
const className = classNames('edge', { highlighted });
return (
<g
id={encodeIdAttribute(id)}
@@ -27,6 +46,11 @@ class Edge extends React.Component {
onMouseLeave={this.handleMouseLeave}
>
<path className="shadow" d={path} style={{ strokeWidth: 10 * thickness }} />
<path
className={getAdjacencyClass(id)}
d={path}
style={{ strokeWidth: 5 }}
/>
<path
className="link"
d={path}
+93 -22
View File
@@ -1,33 +1,104 @@
import React from 'react';
import { Motion } from 'react-motion';
import { connect } from 'react-redux';
import { List as makeList } from 'immutable';
import { GraphNode } from 'weaveworks-ui-components';
import { weakSpring } from 'weaveworks-ui-components/lib/utils/animation';
import {
getMetricValue,
getMetricColor,
} from '../utils/metric-utils';
import { clickNode, enterNode, leaveNode } from '../actions/app-actions';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import { getNodeColor } from '../utils/color-utils';
import MatchedResults from '../components/matched-results';
import { GRAPH_VIEW_MODE } from '../constants/naming';
import Node from './node';
import NodeNetworksOverlay from './node-networks-overlay';
class NodeContainer extends React.Component {
saveRef = (ref) => {
this.ref = ref;
};
const transformedNode = (otherProps, { x, y, k }) => (
<g transform={`translate(${x},${y}) scale(${k})`}>
<Node {...otherProps} />
</g>
);
handleMouseClick = (nodeId, ev) => {
ev.stopPropagation();
trackAnalyticsEvent('scope.node.click', {
layout: GRAPH_VIEW_MODE,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
this.props.clickNode(nodeId, this.props.label, this.ref.getBoundingClientRect());
};
export default class NodeContainer extends React.PureComponent {
render() {
const {
dx, dy, isAnimated, scale, ...forwardedProps
} = this.props;
if (!isAnimated) {
// Show static node for optimized rendering
return transformedNode(forwardedProps, { x: dx, y: dy, k: scale });
}
renderPrependedInfo = () => {
const { showingNetworks, networks } = this.props;
if (!showingNetworks) return null;
return (
// Animate the node if the layout is sufficiently small
<Motion style={{ x: weakSpring(dx), y: weakSpring(dy), k: weakSpring(scale) }}>
{interpolated => transformedNode(forwardedProps, interpolated)}
</Motion>
<NodeNetworksOverlay networks={networks} />
);
};
renderAppendedInfo = () => {
const matchedMetadata = this.props.matches.get('metadata', makeList());
const matchedParents = this.props.matches.get('parents', makeList());
const matchedDetails = matchedMetadata.concat(matchedParents);
return (
<MatchedResults matches={matchedDetails} searchTerms={this.props.searchTerms} />
);
};
render() {
const {
rank, label, pseudo, metric, showingNetworks, networks
} = this.props;
const { hasMetric, height, formattedValue } = getMetricValue(metric);
const metricFormattedValue = !pseudo && hasMetric ? formattedValue : '';
const labelOffset = (showingNetworks && networks) ? 10 : 0;
return (
<GraphNode
id={this.props.id}
shape={this.props.shape}
tag={this.props.tag}
label={this.props.label}
labelMinor={this.props.labelMinor}
labelOffset={labelOffset}
stacked={this.props.stacked}
highlighted={this.props.highlighted}
color={getNodeColor(rank, label, pseudo)}
size={this.props.size}
isAnimated={this.props.isAnimated}
contrastMode={this.props.contrastMode}
forceSvg={this.props.exportingGraph}
searchTerms={this.props.searchTerms}
metricColor={getMetricColor(metric)}
metricFormattedValue={metricFormattedValue}
metricNumericValue={height}
renderPrependedInfo={this.renderPrependedInfo}
renderAppendedInfo={this.renderAppendedInfo}
onMouseEnter={this.props.enterNode}
onMouseLeave={this.props.leaveNode}
onClick={this.handleMouseClick}
graphNodeRef={this.saveRef}
x={this.props.x}
y={this.props.y}
/>
);
}
}
function mapStateToProps(state) {
return {
searchTerms: [state.get('searchQuery')],
exportingGraph: state.get('exportingGraph'),
showingNetworks: state.get('showingNetworks'),
currentTopology: state.get('currentTopology'),
contrastMode: state.get('contrastMode'),
};
}
export default connect(
mapStateToProps,
{ clickNode, enterNode, leaveNode }
)(NodeContainer);
@@ -4,7 +4,6 @@ import { List as makeList } from 'immutable';
import { connect } from 'react-redux';
import { getNetworkColor } from '../utils/color-utils';
import { NODE_BASE_SIZE } from '../constants/styles';
// Min size is about a quarter of the width, feels about right.
const minBarWidth = 0.25;
@@ -14,7 +13,7 @@ const borderRadius = 0.01;
const offset = 0.67;
const x = scaleBand();
function NodeNetworksOverlay({ stack, networks = makeList(), contrastMode }) {
function NodeNetworksOverlay({ networks = makeList() }) {
const barWidth = Math.max(1, minBarWidth * networks.size);
const yPosition = offset - (barHeight * 0.5);
@@ -38,9 +37,8 @@ function NodeNetworksOverlay({ stack, networks = makeList(), contrastMode }) {
/>
));
const translateY = stack && contrastMode ? 0.15 : 0;
return (
<g transform={`translate(0, ${translateY}) scale(${NODE_BASE_SIZE})`}>
<g transform="translate(0, -5) scale(60)">
{bars.toJS()}
</g>
);
@@ -1,24 +0,0 @@
import React from 'react';
import { NODE_BASE_SIZE } from '../constants/styles';
export default function NodeShapeStack(props) {
const verticalDistance = NODE_BASE_SIZE * (props.contrastMode ? 0.12 : 0.1);
const verticalTranslate = t => `translate(0, ${t * verticalDistance})`;
const Shape = props.shape;
// Stack three shapes on top of one another pretending they are never highlighted.
// Instead, fake the highlight of the whole stack with a vertically stretched shape
// drawn in the background. This seems to give a good approximation of the stack
// highlight and prevents us from needing to do some render-heavy SVG clipping magic.
return (
<g transform={verticalTranslate(-2.5)} className="stack">
<g transform={`${verticalTranslate(1)} scale(1, 1.14)`}>
<Shape className="highlight-only" {...props} />
</g>
<g transform={verticalTranslate(2)}><Shape {...props} highlighted={false} /></g>
<g transform={verticalTranslate(1)}><Shape {...props} highlighted={false} /></g>
<g transform={verticalTranslate(0)}><Shape {...props} highlighted={false} /></g>
</g>
);
}
-85
View File
@@ -1,85 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import { NODE_BASE_SIZE } from '../constants/styles';
import {
getMetricValue,
getMetricColor,
getClipPathDefinition,
} from '../utils/metric-utils';
import {
pathElement,
circleElement,
rectangleElement,
circleShapeProps,
triangleShapeProps,
squareShapeProps,
pentagonShapeProps,
hexagonShapeProps,
heptagonShapeProps,
octagonShapeProps,
cloudShapeProps,
} from '../utils/node-shape-utils';
import { encodeIdAttribute } from '../utils/dom-utils';
function NodeShape(shapeType, shapeElement, shapeProps, {
id, highlighted, color, metric
}) {
const { height, hasMetric, formattedValue } = getMetricValue(metric);
const className = classNames('shape', `shape-${shapeType}`, { metrics: hasMetric });
const metricStyle = { fill: getMetricColor(metric) };
const clipId = encodeIdAttribute(`metric-clip-${id}`);
return (
<g className={className}>
{highlighted && shapeElement({
className: 'highlight-border',
transform: `scale(${NODE_BASE_SIZE * 0.5})`,
...shapeProps,
})}
{highlighted && shapeElement({
className: 'highlight-shadow',
transform: `scale(${NODE_BASE_SIZE * 0.5})`,
...shapeProps,
})}
{shapeElement({
className: 'background',
transform: `scale(${NODE_BASE_SIZE * 0.48})`,
...shapeProps,
})}
{hasMetric && getClipPathDefinition(clipId, height, 0.48)}
{hasMetric && shapeElement({
className: 'metric-fill',
transform: `scale(${NODE_BASE_SIZE * 0.48})`,
clipPath: `url(#${clipId})`,
style: metricStyle,
...shapeProps,
})}
{shapeElement({
className: 'shadow',
transform: `scale(${NODE_BASE_SIZE * 0.49})`,
...shapeProps,
})}
{shapeElement({
className: 'border',
transform: `scale(${NODE_BASE_SIZE * 0.5})`,
stroke: color,
...shapeProps,
})}
{hasMetric && highlighted ?
<text>{formattedValue}</text> :
<circle className="node" r={NODE_BASE_SIZE * 0.1} />
}
</g>
);
}
export const NodeShapeCircle = props => NodeShape('circle', circleElement, circleShapeProps, props);
export const NodeShapeTriangle = props => NodeShape('triangle', pathElement, triangleShapeProps, props);
export const NodeShapeSquare = props => NodeShape('square', rectangleElement, squareShapeProps, props);
export const NodeShapePentagon = props => NodeShape('pentagon', pathElement, pentagonShapeProps, props);
export const NodeShapeHexagon = props => NodeShape('hexagon', pathElement, hexagonShapeProps, props);
export const NodeShapeHeptagon = props => NodeShape('heptagon', pathElement, heptagonShapeProps, props);
export const NodeShapeOctagon = props => NodeShape('octagon', pathElement, octagonShapeProps, props);
export const NodeShapeCloud = props => NodeShape('cloud', pathElement, cloudShapeProps, props);
-185
View File
@@ -1,185 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import { Map as makeMap, List as makeList } from 'immutable';
import { clickNode, enterNode, leaveNode } from '../actions/app-actions';
import { getNodeColor } from '../utils/color-utils';
import MatchedText from '../components/matched-text';
import MatchedResults from '../components/matched-results';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import { GRAPH_VIEW_MODE } from '../constants/naming';
import { NODE_BASE_SIZE } from '../constants/styles';
import NodeShapeStack from './node-shape-stack';
import NodeNetworksOverlay from './node-networks-overlay';
import {
NodeShapeCircle,
NodeShapeTriangle,
NodeShapeSquare,
NodeShapePentagon,
NodeShapeHexagon,
NodeShapeHeptagon,
NodeShapeOctagon,
NodeShapeCloud,
} from './node-shapes';
const labelWidth = 1.2 * NODE_BASE_SIZE;
const nodeShapes = {
circle: NodeShapeCircle,
triangle: NodeShapeTriangle,
square: NodeShapeSquare,
pentagon: NodeShapePentagon,
hexagon: NodeShapeHexagon,
heptagon: NodeShapeHeptagon,
octagon: NodeShapeOctagon,
cloud: NodeShapeCloud,
};
function stackedShape(Shape) {
const factory = React.createFactory(NodeShapeStack);
return props => factory(Object.assign({}, props, {shape: Shape}));
}
function getNodeShape({ shape, stack }) {
const nodeShape = nodeShapes[shape];
if (!nodeShape) {
throw new Error(`Unknown shape: ${shape}!`);
}
return stack ? stackedShape(nodeShape) : nodeShape;
}
class Node extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
hovered: false,
};
this.handleMouseClick = this.handleMouseClick.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.saveShapeRef = this.saveShapeRef.bind(this);
}
renderSvgLabels(labelClassName, labelMinorClassName, labelOffsetY) {
const { label, labelMinor } = this.props;
return (
<g className="node-labels-container">
<text className={labelClassName} y={13 + labelOffsetY} textAnchor="middle">{label}</text>
<text className={labelMinorClassName} y={30 + labelOffsetY} textAnchor="middle">
{labelMinor}
</text>
</g>
);
}
renderStandardLabels(labelClassName, labelMinorClassName, labelOffsetY, mouseEvents) {
const { label, labelMinor, matches = makeMap() } = this.props;
const matchedMetadata = matches.get('metadata', makeList());
const matchedParents = matches.get('parents', makeList());
const matchedNodeDetails = matchedMetadata.concat(matchedParents);
return (
<foreignObject
className="node-labels-container"
y={labelOffsetY}
x={-0.5 * labelWidth}
width={labelWidth}
height="5em">
<div className="node-label-wrapper" {...mouseEvents}>
<div className={labelClassName}>
<MatchedText text={label} match={matches.get('label')} />
</div>
<div className={labelMinorClassName}>
<MatchedText text={labelMinor} match={matches.get('labelMinor')} />
</div>
<MatchedResults matches={matchedNodeDetails} />
</div>
</foreignObject>
);
}
render() {
const {
focused, highlighted, networks, pseudo, rank, label, transform,
exportingGraph, showingNetworks, stack, id, metric
} = this.props;
const { hovered } = this.state;
const color = getNodeColor(rank, label, pseudo);
const truncate = !focused && !hovered;
const labelOffsetY = (showingNetworks && networks) ? 40 : 28;
const nodeClassName = classnames('node', { highlighted, hovered, pseudo });
const labelClassName = classnames('node-label', { truncate });
const labelMinorClassName = classnames('node-label-minor', { truncate });
const NodeShapeType = getNodeShape(this.props);
const mouseEvents = {
onClick: this.handleMouseClick,
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave,
};
return (
<g className={nodeClassName} transform={transform}>
{exportingGraph ?
this.renderSvgLabels(labelClassName, labelMinorClassName, labelOffsetY) :
this.renderStandardLabels(labelClassName, labelMinorClassName, labelOffsetY, mouseEvents)}
<g {...mouseEvents} ref={this.saveShapeRef}>
<NodeShapeType
id={id}
highlighted={highlighted}
color={color}
metric={metric}
contrastMode={this.props.contrastMode}
/>
</g>
{showingNetworks && <NodeNetworksOverlay networks={networks} stack={stack} />}
</g>
);
}
saveShapeRef(ref) {
this.shapeRef = ref;
}
handleMouseClick(ev) {
ev.stopPropagation();
trackAnalyticsEvent('scope.node.click', {
layout: GRAPH_VIEW_MODE,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
this.props.clickNode(this.props.id, this.props.label, this.shapeRef.getBoundingClientRect());
}
handleMouseEnter() {
this.props.enterNode(this.props.id);
this.setState({ hovered: true });
}
handleMouseLeave() {
this.props.leaveNode(this.props.id);
this.setState({ hovered: false });
}
}
function mapStateToProps(state) {
return {
exportingGraph: state.get('exportingGraph'),
showingNetworks: state.get('showingNetworks'),
currentTopology: state.get('currentTopology'),
contrastMode: state.get('contrastMode'),
};
}
export default connect(
mapStateToProps,
{ clickNode, enterNode, leaveNode }
)(Node);
@@ -2,6 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { fromJS, Map as makeMap, List as makeList } from 'immutable';
import theme from 'weaveworks-ui-components/lib/theme';
import NodeContainer from './node-container';
import EdgeContainer from './edge-container';
@@ -20,6 +21,7 @@ import {
layoutEdgesSelector
} from '../selectors/graph-view/layout';
import { NODE_BASE_SIZE } from '../constants/styles';
import {
BLURRED_EDGES_LAYER,
BLURRED_NODES_LAYER,
@@ -147,7 +149,11 @@ class NodesChartElements extends React.Component {
}
renderNode(node) {
const { isAnimated, contrastMode } = this.props;
const { isAnimated } = this.props;
// old versions of scope reports have a node shape of `storagesheet`
// if so, normalise to `sheet`
const shape = node.get('shape') === 'storagesheet' ? 'sheet' : node.get('shape');
return (
<NodeContainer
matches={node.get('matches')}
@@ -155,19 +161,19 @@ class NodesChartElements extends React.Component {
metric={node.get('metric')}
focused={node.get('focused')}
highlighted={node.get('highlighted')}
shape={node.get('shape')}
stack={node.get('stack')}
shape={shape}
tag={node.get('tag')}
stacked={node.get('stack')}
key={node.get('id')}
id={node.get('id')}
label={node.get('label')}
labelMinor={node.get('labelMinor')}
pseudo={node.get('pseudo')}
rank={node.get('rank')}
dx={node.get('x')}
dy={node.get('y')}
scale={node.get('scale')}
x={node.get('x')}
y={node.get('y')}
size={node.get('scale') * NODE_BASE_SIZE}
isAnimated={isAnimated}
contrastMode={contrastMode}
/>
);
}
@@ -199,7 +205,7 @@ class NodesChartElements extends React.Component {
className={className}
key="nodes-chart-overlay"
transform={`scale(${scale})`}
fill="#fff"
fill={theme.colors.purple25}
x={-1}
y={-1}
width={2}
+1 -2
View File
@@ -7,13 +7,12 @@ const NodesError = ({
const className = classnames(mainClassName, {
hide: hidden
});
const iconClassName = `fa ${faIconClass}`;
return (
<div className={className}>
<div className="nodes-chart-error-icon-container">
<div className="nodes-chart-error-icon">
<span className={iconClassName} />
<span className={faIconClass} />
</div>
</div>
{children}
+29 -12
View File
@@ -1,12 +1,15 @@
/* eslint react/jsx-no-bind: "off", no-multi-comp: "off" */
import React from 'react';
import styled from 'styled-components';
import { connect } from 'react-redux';
import { List as makeList, Map as makeMap } from 'immutable';
import capitalize from 'lodash/capitalize';
import NodeDetailsTable from '../components/node-details/node-details-table';
import { clickNode, sortOrderChanged } from '../actions/app-actions';
import { shownNodesSelector } from '../selectors/node-filters';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import { findTopologyById } from '../utils/topology-utils';
import { TABLE_VIEW_MODE } from '../constants/naming';
import { windowHeightSelector } from '../selectors/canvas';
@@ -18,7 +21,24 @@ const IGNORED_COLUMNS = ['docker_container_ports', 'docker_container_id', 'docke
'docker_container_command', 'docker_container_networks'];
function getColumns(nodes) {
const Icon = styled.span`
border-radius: ${props => props.theme.borderRadius.soft};
background-color: ${props => props.color};
margin-top: 3px;
display: block;
height: 12px;
width: 12px;
`;
function topologyLabel(topologies, id) {
const topology = findTopologyById(topologies, id);
if (!topology) {
return capitalize(id);
}
return topology.get('fullName');
}
function getColumns(nodes, topologies) {
const metricColumns = nodes
.toList()
.flatMap((n) => {
@@ -43,11 +63,12 @@ function getColumns(nodes) {
.toList()
.sortBy(m => m.get('label'));
const relativesColumns = nodes
.toList()
.flatMap((n) => {
const metadata = (n.get('parents') || makeList())
.map(m => makeMap({ id: m.get('topologyId'), label: m.get('topologyId') }));
.map(m => makeMap({ id: m.get('topologyId'), label: topologyLabel(topologies, m.get('topologyId')) }));
return metadata;
})
.toSet()
@@ -63,23 +84,18 @@ function renderIdCell({
}) {
const showSubLabel = Boolean(pseudo) && labelMinor;
const title = showSubLabel ? `${label} (${labelMinor})` : label;
const iconStyle = {
width: 16,
flex: 'none',
color: getNodeColor(rank, label)
};
return (
<div title={title} className="nodes-grid-id-column">
<div style={iconStyle}><i className="fa fa-square" /></div>
<div style={{ width: 16, flex: 'none' }}>
<Icon color={getNodeColor(rank, label)} />
</div>
<div className="truncate">
{label} {showSubLabel && <span className="nodes-grid-label-minor">{labelMinor}</span>}
</div>
</div>
);
}
class NodesGrid extends React.Component {
constructor(props, context) {
super(props, context);
@@ -108,7 +124,7 @@ class NodesGrid extends React.Component {
render() {
const {
nodes, gridSortedBy, gridSortedDesc, searchNodeMatches, searchQuery, windowHeight
nodes, gridSortedBy, gridSortedDesc, searchNodeMatches, searchQuery, windowHeight, topologies
} = this.props;
const height =
this.tableRef ? windowHeight - this.tableRef.getBoundingClientRect().top - 30 : 0;
@@ -131,7 +147,7 @@ class NodesGrid extends React.Component {
.toList()
.filter(n => !(searchQuery && searchNodeMatches.get(n.get('id'), makeMap()).isEmpty()))
.toJS(),
columns: getColumns(nodes)
columns: getColumns(nodes, topologies)
};
return (
@@ -167,6 +183,7 @@ function mapStateToProps(state) {
searchQuery: state.get('searchQuery'),
selectedNodeId: state.get('selectedNodeId'),
windowHeight: windowHeightSelector(state),
topologies: state.get('topologies'),
};
}
+30 -16
View File
@@ -1,8 +1,9 @@
import debug from 'debug';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { debounce, isEqual } from 'lodash';
import { ThemeProvider } from 'styled-components';
import theme from 'weaveworks-ui-components/lib/theme';
@@ -11,7 +12,6 @@ import Logo from './logo';
import Footer from './footer';
import Sidebar from './sidebar';
import HelpPanel from './help-panel';
import CloudFeature from './cloud-feature';
import TroubleshootingMenu from './troubleshooting-menu';
import Search from './search';
import Status from './status';
@@ -23,7 +23,6 @@ import {
focusSearch,
pinNextMetric,
pinPreviousMetric,
hitBackspace,
hitEsc,
unpinMetric,
toggleHelp,
@@ -31,6 +30,7 @@ import {
setMonitorState,
setTableView,
setResourceView,
setStoreViewState,
shutdown,
setViewportDimensions,
getTopologiesWithInitialPoll,
@@ -53,21 +53,21 @@ import {
} from '../selectors/topology';
import { VIEWPORT_RESIZE_DEBOUNCE_INTERVAL } from '../constants/timer';
import {
BACKSPACE_KEY_CODE,
ESC_KEY_CODE,
} from '../constants/key-codes';
const keyPressLog = debug('scope:app-key-press');
class App extends React.Component {
constructor(props, context) {
super(props, context);
this.props.dispatch(setMonitorState(this.props.monitor));
this.props.dispatch(setStoreViewState(!this.props.disableStoreViewState));
this.setViewportDimensions = this.setViewportDimensions.bind(this);
this.handleResize = debounce(this.setViewportDimensions, VIEWPORT_RESIZE_DEBOUNCE_INTERVAL);
this.handleRouteChange = debounce(props.onRouteChange, 50);
this.saveAppRef = this.saveAppRef.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
@@ -80,7 +80,7 @@ class App extends React.Component {
window.addEventListener('keypress', this.onKeyPress);
window.addEventListener('keyup', this.onKeyUp);
this.router = getRouter(this.props.dispatch, this.props.urlState);
this.router = this.props.dispatch(getRouter(this.props.urlState));
this.router.start({ hashbang: true });
if (!this.props.routeSet || process.env.WEAVE_CLOUD) {
@@ -103,6 +103,13 @@ class App extends React.Component {
if (nextProps.monitor !== this.props.monitor) {
this.props.dispatch(setMonitorState(nextProps.monitor));
}
if (nextProps.disableStoreViewState !== this.props.disableStoreViewState) {
this.props.dispatch(setStoreViewState(!nextProps.disableStoreViewState));
}
// Debounce-notify about the route change if the URL state changes its content.
if (!isEqual(nextProps.urlState, this.props.urlState)) {
this.handleRouteChange(nextProps.urlState);
}
}
onKeyUp(ev) {
@@ -112,8 +119,6 @@ class App extends React.Component {
// don't get esc in onKeyPress
if (ev.keyCode === ESC_KEY_CODE) {
this.props.dispatch(hitEsc());
} else if (ev.keyCode === BACKSPACE_KEY_CODE) {
this.props.dispatch(hitBackspace());
} else if (ev.code === 'KeyD' && ev.ctrlKey && !showingTerminal) {
toggleDebugToolbar();
this.forceUpdate();
@@ -202,14 +207,12 @@ class App extends React.Component {
{showingTroubleshootingMenu && <TroubleshootingMenu />}
{showingDetails && <Details />}
{showingDetails && <Details
renderNodeDetailsExtras={this.props.renderNodeDetailsExtras}
/>}
<div className="header">
{timeTravelSupported && (
<CloudFeature alwaysShow>
<TimeTravelWrapper />
</CloudFeature>
)}
{timeTravelSupported && this.props.renderTimeTravel()}
<div className="selectors">
<div className="logo">
@@ -243,7 +246,6 @@ class App extends React.Component {
}
}
function mapStateToProps(state) {
return {
currentTopology: state.get('currentTopology'),
@@ -266,8 +268,20 @@ function mapStateToProps(state) {
};
}
App.propTypes = {
renderTimeTravel: PropTypes.func,
renderNodeDetailsExtras: PropTypes.func,
onRouteChange: PropTypes.func,
monitor: PropTypes.bool,
disableStoreViewState: PropTypes.bool,
};
App.defaultProps = {
monitor: false
renderTimeTravel: () => <TimeTravelWrapper />,
renderNodeDetailsExtras: () => null,
onRouteChange: () => null,
monitor: false,
disableStoreViewState: false,
};
export default connect(mapStateToProps)(App);
+3 -3
View File
@@ -9,7 +9,7 @@ import CloudFeature from './cloud-feature';
* that is comprised of Weave Cloud related pieces.
*
* We support here relative links with a leading `/` that rewrite
* the browser url as well as cloud-related placeholders (:orgId).
* the browser url as well as cloud-related placeholders (:instanceId).
*
* If no `url` is given, only the children is rendered (no anchor).
*
@@ -50,8 +50,8 @@ class LinkWrapper extends React.Component {
buildHref(url) {
const { params } = this.props;
if (!url || !params || !params.orgId) return url;
return url.replace(/:orgid/gi, encodeURIComponent(this.props.params.orgId));
if (!url || !params || !params.instanceId) return url;
return url.replace(/:instanceid/gi, encodeURIComponent(params.instanceId));
}
render() {
@@ -60,6 +60,7 @@ class DetailsCard extends React.Component {
key={this.props.id}
nodeId={this.props.id}
mounted={this.state.mounted}
renderNodeDetailsExtras={this.props.renderNodeDetailsExtras}
{...this.props}
/>
</div>
+1
View File
@@ -15,6 +15,7 @@ class Details extends React.Component {
index={index}
cardCount={details.size}
nodeControlStatus={controlStatus.get(obj.id)}
renderNodeDetailsExtras={this.props.renderNodeDetailsExtras}
{...obj}
/>
))}
+4 -4
View File
@@ -74,10 +74,10 @@ class Footer extends React.Component {
className="footer-icon"
onClick={this.handleRelayoutClick}
title={forceRelayoutTitle}>
<span className="fa fa-refresh" />
<i className="fa fa-sync" />
</button>
<button onClick={this.handleContrastClick} className="footer-icon" title={otherContrastModeTitle}>
<span className="fa fa-adjust" />
<i className="fa fa-adjust" />
</button>
<button
onClick={this.props.toggleTroubleshootingMenu}
@@ -85,10 +85,10 @@ class Footer extends React.Component {
title="Open troubleshooting menu"
href=""
>
<span className="fa fa-bug" />
<i className="fa fa-bug" />
</button>
<button className="footer-icon" onClick={this.props.toggleHelp} title="Show help">
<span className="fa fa-question" />
<i className="fa fa-question" />
</button>
</div>
+2 -2
View File
@@ -172,9 +172,9 @@ function HelpPanel({
{renderFieldsPanel(currentTopologyName, searchableFields)}
</div>
<div className="help-panel-tools">
<span
<i
title="Close details"
className="fa fa-close"
className="fa fa-times"
onClick={onClickClose}
/>
</div>
+1 -1
View File
@@ -48,7 +48,7 @@ export class Loading extends React.Component {
const { itemType, show } = this.props;
const message = renderTemplate(itemType, this.state.template);
return (
<NodesError mainClassName="nodes-chart-loading" faIconClass="fa-circle-thin" hidden={!show}>
<NodesError mainClassName="nodes-chart-loading" faIconClass="far fa-circle" hidden={!show}>
<div className="heading">{message}</div>
</NodesError>
);
@@ -1,12 +1,9 @@
import React from 'react';
import MatchedText from './matched-text';
import { MatchedText } from 'weaveworks-ui-components';
const SHOW_ROW_COUNT = 2;
const MAX_MATCH_LENGTH = 24;
const Match = match => (
const Match = (searchTerms, match) => (
<div className="matched-results-match" key={match.label}>
<div className="matched-results-match-wrapper">
<span className="matched-results-match-label">
@@ -14,16 +11,15 @@ const Match = match => (
</span>
<MatchedText
text={match.text}
match={match}
maxLength={MAX_MATCH_LENGTH}
truncate={match.truncate} />
matches={searchTerms}
/>
</div>
</div>
);
export default class MatchedResults extends React.PureComponent {
render() {
const { matches, style } = this.props;
const { matches, searchTerms, style } = this.props;
if (!matches) {
return null;
@@ -41,7 +37,11 @@ export default class MatchedResults extends React.PureComponent {
return (
<div className="matched-results" style={style}>
{matches.keySeq().take(SHOW_ROW_COUNT).map(fieldId => Match(matches.get(fieldId)))}
{matches
.keySeq()
.take(SHOW_ROW_COUNT)
.map(fieldId => Match(searchTerms, matches.get(fieldId)))
}
{moreFieldMatches &&
<div className="matched-results-more" title={moreFieldMatchesTitle}>
{`${moreFieldMatches.size} more matches`}
@@ -58,7 +58,7 @@ class MetricSelectorItem extends React.Component {
onMouseOver={this.onMouseOver}
onClick={this.onMouseClick}>
{type}
{isPinned && <span className="fa fa-thumb-tack" />}
{isPinned && <i className="fa fa-thumbtack" />}
</div>
);
}
@@ -49,7 +49,7 @@ class NetworkSelectorItem extends React.Component {
onClick={this.onMouseClick}
style={style}>
{network.get('label')}
{isPinned && <span className="fa fa-thumb-tack" />}
{isPinned && <i className="fa fa-thumbtack" />}
</div>
);
}
+18 -19
View File
@@ -1,8 +1,10 @@
import debug from 'debug';
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Map as makeMap } from 'immutable';
import { noop } from 'lodash';
import { clickCloseDetails, clickShowTopologyForNode } from '../actions/app-actions';
import { brightenColor, getNeutralColor, getNodeColorDark } from '../utils/color-utils';
@@ -19,8 +21,6 @@ import NodeDetailsInfo from './node-details/node-details-info';
import NodeDetailsRelatives from './node-details/node-details-relatives';
import NodeDetailsTable from './node-details/node-details-table';
import Warning from './warning';
import CloudFeature from './cloud-feature';
import NodeDetailsImageStatus from './node-details/node-details-image-status';
const log = debug('scope:node-details');
@@ -57,16 +57,16 @@ class NodeDetails extends React.Component {
<div className="node-details-tools-wrapper">
<div className="node-details-tools">
{showSwitchTopology &&
<span
<i
title={topologyTitle}
className="fa fa-long-arrow-left"
className="fa fa-long-arrow-alt-left"
onClick={this.handleShowTopologyForNode}>
<span>Show in <span>{this.props.topologyId.replace(/-/g, ' ')}</span></span>
</span>
</i>
}
<span
<i
title="Close details"
className="fa fa-close close-details"
className="fa fa-times close-details"
onClick={this.handleClickClose}
/>
</div>
@@ -80,7 +80,7 @@ class NodeDetails extends React.Component {
// NOTE: If we start the fa-spin animation before the node details panel has been
// mounted, the spinner is displayed blurred the whole time in Chrome (possibly
// caused by a bug having to do with animating the details panel).
const spinnerClassName = classNames('fa fa-circle-o-notch', { 'fa-spin': this.props.mounted });
const spinnerClassName = classNames('fa fa-circle-notch', { 'fa-spin': this.props.mounted });
const nodeColor = (node ?
getNodeColorDark(node.get('rank'), label, node.get('pseudo')) :
getNeutralColor());
@@ -130,9 +130,7 @@ class NodeDetails extends React.Component {
</div>
<div className="node-details-content">
<p className="node-details-content-info">
<strong>{this.props.label}</strong> is not visible to Scope when it
is not communicating.
Details will become available here when it communicates again.
<strong>{this.props.label}</strong> not found!
</p>
</div>
<Overlay faded={this.props.transitioning} />
@@ -249,14 +247,7 @@ class NodeDetails extends React.Component {
return null;
})}
<CloudFeature>
<NodeDetailsImageStatus
name={details.label}
metadata={details.metadata}
pseudo={details.pseudo}
topologyId={topologyId}
/>
</CloudFeature>
{this.props.renderNodeDetailsExtras({ topologyId, details })}
</div>
<Overlay faded={this.props.transitioning} />
@@ -298,6 +289,14 @@ class NodeDetails extends React.Component {
}
}
NodeDetails.propTypes = {
renderNodeDetailsExtras: PropTypes.func,
};
NodeDetails.defaultProps = {
renderNodeDetailsExtras: noop,
};
function mapStateToProps(state, ownProps) {
const currentTopologyId = state.get('currentTopologyId');
return {
@@ -15,7 +15,7 @@ describe('NodeDetailsHealthLinkItem', () => {
});
it('appends as json for cloud link', () => {
const url = appendTime('/prom/:orgid/notebook/new/%7B%22cells%22%3A%5B%7B%22queries%22%3A%5B%22go_goroutines%22%5D%7D%5D%7D', time);
const url = appendTime('/prom/:instanceid/notebook/new/%7B%22cells%22%3A%5B%7B%22queries%22%3A%5B%22go_goroutines%22%5D%7D%5D%7D', time);
expect(url).toContain(timeUnix);
const payload = JSON.parse(decodeURIComponent(url.substr(url.indexOf('new/') + 4)));
@@ -1,5 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { trackAnalyticsEvent } from '../../utils/tracking-utils';
import { doControl } from '../../actions/app-actions';
@@ -11,12 +12,14 @@ class NodeDetailsControlButton extends React.Component {
}
render() {
let className = `tour-step-anchor node-control-button fa ${this.props.control.icon}`;
if (this.props.pending) {
className += ' node-control-button-pending';
}
const { icon, human } = this.props.control;
const className = classNames('tour-step-anchor node-control-button', icon, {
'node-control-button-pending': this.props.pending,
// Old Agent / plugins don't include the 'fa ' prefix, so provide it if they don't.
fa: icon.startsWith('fa-')
});
return (
<span className={className} title={this.props.control.human} onClick={this.handleClick} />
<i className={className} title={human} onClick={this.handleClick} />
);
}
@@ -6,7 +6,7 @@ import NodeDetailsControlButton from './node-details-control-button';
export default function NodeDetailsControls({
controls, error, nodeId, pending
}) {
let spinnerClassName = 'fa fa-circle-o-notch fa-spin';
let spinnerClassName = 'fa fa-circle-notch fa-spin';
if (pending) {
spinnerClassName += ' node-details-controls-spinner';
} else {
@@ -17,7 +17,7 @@ export default function NodeDetailsControls({
<div className="node-details-controls">
{error &&
<div className="node-details-controls-error" title={error}>
<span className="node-details-controls-error-icon fa fa-warning" />
<i className="node-details-controls-error-icon fa fa-exclamation-triangle" />
<span className="node-details-controls-error-messages">{error}</span>
</div>
}
@@ -1,126 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import find from 'lodash/find';
import map from 'lodash/map';
import { CircularProgress } from 'weaveworks-ui-components';
import { getImagesForService } from '../../actions/app-actions';
const topologyWhitelist = ['kube-controllers'];
function newImagesAvailable(images, currentId) {
const current = find(images, i => i.ID === currentId);
if (current) {
const timestamp = new Date(current.CreatedAt);
return Boolean(find(images, i => new Date(i.CreatedAt) > timestamp));
}
return false;
}
class NodeDetailsImageStatus extends React.PureComponent {
constructor(props, context) {
super(props, context);
this.getImagesUrl = this.getImagesUrl.bind(this);
}
componentDidMount() {
if (this.shouldRender() && this.props.serviceId) {
this.props.getImagesForService(this.props.params.orgId, this.props.serviceId);
}
}
getImagesUrl() {
const { serviceId, params } = this.props;
return `/flux/${params.orgId}/services/${encodeURIComponent(serviceId)}`;
}
shouldRender() {
const { pseudo, topologyId } = this.props;
return !pseudo && topologyId && topologyWhitelist.includes(topologyId);
}
renderImages() {
const { errors, containers, isFetching } = this.props;
const error = !isFetching && errors;
if (isFetching) {
return (
<div className="progress-wrapper"><CircularProgress /></div>
);
}
if (error) {
return (
<p>Error: {JSON.stringify(map(errors, 'message'))}</p>
);
}
if (!containers) {
return 'No service images found';
}
return (
<div className="images">
{containers.map((container) => {
const statusText = newImagesAvailable(container.Available, container.Current.ID)
? <span className="new-image">New image(s) available</span>
: 'Image up to date';
return (
<div key={container.Name} className="wrapper">
<div className="node-details-table-node-label">{container.Name}</div>
<div className="node-details-table-node-value">{statusText}</div>
</div>
);
})}
</div>
);
}
render() {
const { containers } = this.props;
if (!this.shouldRender()) {
return null;
}
return (
<div className="node-details-content-section image-status">
<div className="node-details-content-section-header">
Container Image Status
{containers &&
<div>
<a
href={this.getImagesUrl()}
className="node-details-table-node-link">
View in Deploy
</a>
</div>
}
</div>
{this.renderImages()}
</div>
);
}
}
function mapStateToProps({ scope }, { metadata, name }) {
const namespace = find(metadata, d => d.id === 'kubernetes_namespace');
const nodeType = find(metadata, d => d.id === 'kubernetes_node_type');
const serviceId = (namespace && nodeType) ? `${namespace.value}:${nodeType.value.toLowerCase()}/${name}` : null;
const { containers, isFetching, errors } = scope.getIn(['serviceImages', serviceId]) || {};
return {
isFetching,
errors,
containers,
serviceId
};
}
export default connect(mapStateToProps, { getImagesForService })(NodeDetailsImageStatus);
@@ -41,9 +41,9 @@ export default class NodeDetailsTableHeaders extends React.Component {
<td className={headerClasses.join(' ')} style={style} title={header.label} key={header.id}>
<div className="node-details-table-header-sortable" onClick={onClick}>
{isSortedAsc
&& <span className="node-details-table-header-sorter fa fa-caret-up" />}
&& <i className="node-details-table-header-sorter fa fa-caret-up" />}
{isSortedDesc
&& <span className="node-details-table-header-sorter fa fa-caret-down" />}
&& <i className="node-details-table-header-sorter fa fa-caret-down" />}
{label}
</div>
</td>
@@ -145,6 +145,7 @@ class NodeDetailsTable extends React.Component {
this.onMouseLeaveRow = this.onMouseLeaveRow.bind(this);
this.onMouseEnterRow = this.onMouseEnterRow.bind(this);
this.saveTableContentRef = this.saveTableContentRef.bind(this);
this.saveTableHeadRef = this.saveTableHeadRef.bind(this);
// Use debouncing to prevent event flooding when e.g. crossing fast with mouse cursor
// over the whole table. That would be expensive as each focus causes table to rerender.
this.debouncedFocusRow = debounce(this.focusRow, TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL);
@@ -172,7 +173,7 @@ class NodeDetailsTable extends React.Component {
this.focusState = {
focusedNode: node,
focusedRowIndex: rowIndex,
tableContentMinHeightConstraint: this.tableContent && this.tableContent.scrollHeight,
tableContentMinHeightConstraint: this.tableContentRef && this.tableContentRef.scrollHeight,
};
}
@@ -192,7 +193,11 @@ class NodeDetailsTable extends React.Component {
}
saveTableContentRef(ref) {
this.tableContent = ref;
this.tableContentRef = ref;
}
saveTableHeadRef(ref) {
this.tableHeadRef = ref;
}
getColumnHeaders() {
@@ -200,6 +205,11 @@ class NodeDetailsTable extends React.Component {
return [{id: 'label', label: this.props.label}].concat(columns);
}
componentDidMount() {
const scrollbarWidth = this.tableContentRef.offsetWidth - this.tableContentRef.clientWidth;
this.tableHeadRef.style.paddingRight = `${scrollbarWidth}px`;
}
render() {
const {
nodeIdKey, columns, topologyId, onClickRow,
@@ -250,7 +260,7 @@ class NodeDetailsTable extends React.Component {
<div className={className} style={this.props.style}>
<div className="node-details-table-wrapper">
<table className="node-details-table">
<thead>
<thead ref={this.saveTableHeadRef}>
{this.props.nodes && this.props.nodes.length > 0 && <NodeDetailsTableHeaders
headers={headers}
sortedBy={sortedBy}
@@ -1,5 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import theme from 'weaveworks-ui-components/lib/theme';
import NodeResourcesMetricBoxInfo from './node-resources-metric-box-info';
import { clickNode } from '../../actions/app-actions';
@@ -120,10 +121,22 @@ class NodeResourcesMetricBox extends React.Component {
className="node-resources-metric-box"
style={{ opacity }}
onClick={this.handleClick}
ref={this.saveNodeRef}>
ref={this.saveNodeRef}
>
<title>{label} - {type} usage at {resourceUsageTooltipInfo}</title>
{showCapacity && <rect className="frame" {...this.defaultRectProps()} />}
<rect className="bar" fill={color} {...this.defaultRectProps(relativeConsumption)} />
{showCapacity && <rect
className="frame"
rx={theme.borderRadius.soft}
ry={theme.borderRadius.soft}
{...this.defaultRectProps()}
/>}
<rect
className="bar"
fill={color}
rx={theme.borderRadius.soft}
ry={theme.borderRadius.soft}
{...this.defaultRectProps(relativeConsumption)}
/>
{showInfo && <NodeResourcesMetricBoxInfo
label={label}
metricSummary={metricSummary}
+1 -1
View File
@@ -42,7 +42,7 @@ class Nodes extends React.Component {
const { topologyNodeCountZero, nodesDisplayEmpty } = this.props;
return (
<NodesError faIconClass="fa-circle-thin" hidden={!nodesDisplayEmpty}>
<NodesError faIconClass="far fa-circle" hidden={!nodesDisplayEmpty}>
<div className="heading">Nothing to show. This can have any of these reasons:</div>
{topologyNodeCountZero ?
renderCauses(NODES_STATS_COUNT_ZERO_CAUSES) :
+1 -1
View File
@@ -17,7 +17,7 @@ const Plugin = ({
<span className="plugins-plugin" key={id}>
<Tooltip tip={tip}>
<span className={className}>
{error && <span className="plugins-plugin-icon fa fa-exclamation-circle" />}
{error && <i className="plugins-plugin-icon fa fa-exclamation-circle" />}
{label || id}
</span>
</Tooltip>
@@ -1,27 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { unpinSearch } from '../actions/app-actions';
class SearchItem extends React.Component {
constructor(props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}
handleClick(ev) {
ev.preventDefault();
this.props.unpinSearch(this.props.query);
}
render() {
return (
<span className="search-item">
<span className="search-item-label">{this.props.query}</span>
<span className="search-item-icon fa fa-close" onClick={this.handleClick} />
</span>
);
}
}
export default connect(null, { unpinSearch })(SearchItem);
+80 -143
View File
@@ -1,19 +1,55 @@
import React from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import { debounce } from 'lodash';
import { isEmpty } from 'lodash';
import { Search } from 'weaveworks-ui-components';
import styled from 'styled-components';
import { blurSearch, doSearch, focusSearch, pinSearch, toggleHelp } from '../actions/app-actions';
import { blurSearch, focusSearch, updateSearch, toggleHelp } from '../actions/app-actions';
import { searchMatchCountByTopologySelector } from '../selectors/search';
import { isResourceViewModeSelector } from '../selectors/topology';
import { slugify } from '../utils/string-utils';
import { parseQuery } from '../utils/search-utils';
import { isTopologyNodeCountZero } from '../utils/topology-utils';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import SearchItem from './search-item';
import { ENTER_KEY_CODE } from '../constants/key-codes';
const SearchWrapper = styled.div`
margin: 0 8px;
min-width: 160px;
text-align: right;
`;
const SearchContainer = styled.div`
display: inline-block;
position: relative;
pointer-events: all;
line-height: 100%;
max-width: 400px;
width: 100%;
`;
const SearchHint = styled.div`
font-size: ${props => props.theme.fontSizes.tiny};
color: ${props => props.theme.colors.purple400};
transition: transform 0.3s 0s ease-in-out, opacity 0.3s 0s ease-in-out;
text-align: left;
margin-top: 3px;
padding: 0 1em;
opacity: 0;
${props => props.active && `
opacity: 1;
`};
`;
const SearchHintIcon = styled.span`
font-size: ${props => props.theme.fontSizes.normal};
cursor: pointer;
&:hover {
color: ${props => props.theme.colors.purple600};
}
`;
function shortenHintLabel(text) {
return text
.split(' ')[0]
@@ -21,7 +57,6 @@ function shortenHintLabel(text) {
.substr(0, 12);
}
// dynamic hint based on node names
function getHint(nodes) {
let label = 'mycontainer';
@@ -39,145 +74,46 @@ function getHint(nodes) {
}
}
return `Try "${label}", "${metadataLabel}:${metadataValue}", or "cpu > 2%".
Hit enter to apply the search as a filter.`;
return `Try "${label}", "${metadataLabel}:${metadataValue}", or "cpu > 2%".`;
}
class Search extends React.Component {
constructor(props, context) {
super(props, context);
this.handleBlur = this.handleBlur.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.saveQueryInputRef = this.saveQueryInputRef.bind(this);
this.doSearch = debounce(this.doSearch.bind(this), 200);
this.state = {
value: ''
};
}
handleBlur() {
this.props.blurSearch();
}
handleChange(ev) {
const inputValue = ev.target.value;
let value = inputValue;
// In render() props.searchQuery can be set from the outside, but state.value
// must have precedence for quick feedback. Now when the user backspaces
// quickly enough from `text`, a previouse doSearch(`text`) will come back
// via props and override the empty state.value. To detect this edge case
// we instead set value to null when backspacing.
if (this.state.value && value === '') {
value = null;
}
this.setState({ value });
this.doSearch(inputValue);
}
handleKeyUp(ev) {
// If the search query is parsable, pin it when ENTER key is hit.
if (ev.keyCode === ENTER_KEY_CODE && parseQuery(this.props.searchQuery)) {
trackAnalyticsEvent('scope.search.query.pin', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
this.props.pinSearch();
}
}
handleFocus() {
this.props.focusSearch();
}
doSearch(value) {
if (value !== '') {
trackAnalyticsEvent('scope.search.query.change', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
}
this.props.doSearch(value);
}
saveQueryInputRef(ref) {
this.queryInput = ref;
}
componentWillReceiveProps(nextProps) {
// when cleared from the outside, reset internal state
if (this.props.searchQuery !== nextProps.searchQuery && nextProps.searchQuery === '') {
this.setState({ value: '' });
}
}
componentDidUpdate() {
if (this.props.searchFocused) {
this.queryInput.focus();
} else if (!this.state.value) {
this.queryInput.blur();
}
class SearchComponent extends React.Component {
handleChange = (searchQuery, pinnedSearches) => {
trackAnalyticsEvent('scope.search.query.change', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
this.props.updateSearch(searchQuery, pinnedSearches);
}
render() {
const {
nodes, pinnedSearches, searchFocused, searchMatchCountByTopology,
isResourceViewMode, searchQuery, topologiesLoaded, inputId = 'search'
searchHint, searchMatchesCount, searchQuery, pinnedSearches, topologiesLoaded,
isResourceViewMode, isTopologyEmpty,
} = this.props;
const hidden = !topologiesLoaded || isResourceViewMode;
const disabled = this.props.isTopologyNodeCountZero && !hidden;
const matchCount = searchMatchCountByTopology
.reduce((count, topologyMatchCount) => count + topologyMatchCount, 0);
const showPinnedSearches = pinnedSearches.size > 0;
// manual clear (null) has priority, then props, then state
const value = this.state.value === null ? '' : this.state.value || searchQuery || '';
const classNames = classnames('search', 'hideable', {
hide: hidden,
'search-pinned': showPinnedSearches,
'search-matched': matchCount,
'search-filled': value,
'search-focused': searchFocused,
'search-disabled': disabled
});
const title = matchCount ? `${matchCount} matches` : null;
return (
<div className="search-wrapper">
<div className={classNames} title={title}>
<div className="search-input">
{showPinnedSearches && pinnedSearches.toIndexedSeq()
.map(query => <SearchItem query={query} key={query} />)}
<input
className="search-input-field"
type="text"
id={inputId}
value={value}
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
disabled={disabled}
ref={this.saveQueryInputRef} />
</div>
<div className="search-label">
<i className="fa fa-search search-label-icon" />
<span className="search-label-hint" htmlFor={inputId}>
Search
</span>
</div>
{!showPinnedSearches &&
<div className="search-hint">
{getHint(nodes)} <span
className="search-help-link fa fa-question-circle"
onMouseDown={this.props.toggleHelp} />
</div>
}
</div>
</div>
<SearchWrapper>
<SearchContainer title={searchMatchesCount ? `${searchMatchesCount} matches` : undefined}>
<Search
placeholder="search"
query={searchQuery}
pinnedTerms={pinnedSearches}
disabled={topologiesLoaded && !isResourceViewMode && isTopologyEmpty}
onChange={this.handleChange}
onFocus={this.props.focusSearch}
onBlur={this.props.blurSearch}
/>
<SearchHint active={this.props.searchFocused && isEmpty(pinnedSearches)}>
{searchHint} <SearchHintIcon
className="fa fa-question-circle"
onMouseDown={this.props.toggleHelp}
/>
</SearchHint>
</SearchContainer>
</SearchWrapper>
);
}
}
@@ -185,18 +121,19 @@ class Search extends React.Component {
export default connect(
state => ({
nodes: state.get('nodes'),
searchHint: getHint(state.get('nodes')),
searchFocused: state.get('searchFocused'),
topologyViewMode: state.get('topologyViewMode'),
isResourceViewMode: isResourceViewModeSelector(state),
isTopologyNodeCountZero: isTopologyNodeCountZero(state),
isTopologyEmpty: isTopologyNodeCountZero(state),
currentTopology: state.get('currentTopology'),
topologiesLoaded: state.get('topologiesLoaded'),
pinnedSearches: state.get('pinnedSearches'),
searchFocused: state.get('searchFocused'),
pinnedSearches: state.get('pinnedSearches').toJS(),
searchQuery: state.get('searchQuery'),
searchMatchCountByTopology: searchMatchCountByTopologySelector(state),
searchMatchesCount: searchMatchCountByTopologySelector(state)
.reduce((count, topologyMatchCount) => count + topologyMatchCount, 0),
}),
{
blurSearch, doSearch, focusSearch, pinSearch, toggleHelp
blurSearch, focusSearch, updateSearch, toggleHelp
}
)(Search);
)(SearchComponent);
+1 -1
View File
@@ -38,7 +38,7 @@ class Status extends React.Component {
return (
<div className={classNames}>
{showWarningIcon && <span className="status-icon fa fa-exclamation-circle" />}
{showWarningIcon && <i className="status-icon fa fa-exclamation-circle" />}
<span className="status-label" title={title}>{text}</span>
</div>
);
+36 -84
View File
@@ -4,9 +4,10 @@ import React from 'react';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { debounce } from 'lodash';
import Term from 'xterm';
import { Terminal as Term } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
import { clickCloseTerminal } from '../actions/app-actions';
import { closeTerminal } from '../actions/app-actions';
import { getNeutralColor } from '../utils/color-utils';
import { setDocumentTitle } from '../utils/title-utils';
import { getPipeStatus, deletePipe, doResizeTty, getWebsocketUrl, basePath } from '../utils/web-api-utils';
@@ -30,32 +31,6 @@ function ab2str(buf) {
return decodedString;
}
function terminalCellSize(wrapperNode) {
// Badly guess the width/height of the row.
let characterWidth = 20;
let characterHeight = 20;
// Now try and measure the first row we find.
const subjectRow = wrapperNode.querySelector('.terminal .xterm-rows div');
if (!subjectRow) {
log("ERROR: Couldn't find first row, resizing might not work very well.");
} else {
const rowDisplay = subjectRow.style.display;
const contentBuffer = subjectRow.innerHTML;
subjectRow.innerHTML = 'W';
subjectRow.style.display = 'inline';
characterWidth = subjectRow.getBoundingClientRect().width;
subjectRow.style.display = rowDisplay;
characterHeight = parseInt(subjectRow.offsetHeight, 10);
subjectRow.innerHTML = contentBuffer;
}
log('Calculated (charWidth, charHeight) sizes in px: ', characterWidth, characterHeight);
return {characterWidth, characterHeight};
}
function openNewWindow(url, bcr, minWidth = 200) {
const screenLeft = window.screenX || window.screenLeft;
const screenTop = window.screenY || window.screenTop;
@@ -89,8 +64,6 @@ class Terminal extends React.Component {
detached: false,
rows: DEFAULT_ROWS,
cols: DEFAULT_COLS,
characterWidth: 0,
characterHeight: 0
};
this.handleCloseClick = this.handleCloseClick.bind(this);
@@ -157,6 +130,10 @@ class Terminal extends React.Component {
if (this.props.connect !== nextProps.connect && nextProps.connect) {
this.mountTerminal();
}
// Close the terminal window immediately when the pipe is deleted.
if (nextProps.pipe.get('status') === 'PIPE_DELETED') {
this.props.dispatch(closeTerminal(this.getPipeId()));
}
}
componentDidMount() {
@@ -167,32 +144,44 @@ class Terminal extends React.Component {
}
mountTerminal() {
Term.applyAddon(fit);
this.term = new Term({
cols: this.state.cols,
rows: this.state.rows,
//
// Some linux systems fail to render 'monospace' on `<canvas>` correctly:
// https://github.com/xtermjs/xterm.js/issues/1170
// `theme.fontFamilies.monospace` doesn't provide many options so we add
// some here that are very common. The alternative _might_ be to bundle Roboto-Mono
//
fontFamily: '"Roboto Mono", "Courier New", "Courier", monospace',
// `theme.fontSizes.tiny` (`"12px"`) is a string and we need an int here.
fontSize: 12,
convertEol: !this.props.pipe.get('raw'),
cursorBlink: true,
scrollback: 10000,
});
this.term.open(this.innerFlex);
this.term.focus();
this.term.on('data', (data) => {
if (this.socket) {
this.socket.send(data);
}
});
this.createWebsocket(this.term);
this.term.on('resize', ({ cols, rows }) => {
const resizeTtyControl = this.props.pipe.get('resizeTtyControl');
if (resizeTtyControl) {
doResizeTty(this.getPipeId(), resizeTtyControl, cols, rows);
}
this.setState({ cols, rows });
});
const {characterWidth, characterHeight} = terminalCellSize(this.term.element);
this.createWebsocket(this.term);
window.addEventListener('resize', this.handleResizeDebounced);
this.resizeTimeout = setTimeout(() => {
this.setState({
characterWidth,
characterHeight
});
this.handleResize();
}, 10);
}
@@ -225,14 +214,7 @@ class Terminal extends React.Component {
}
}
componentDidUpdate(prevProps, prevState) {
const sizeChanged = (
prevState.cols !== this.state.cols ||
prevState.rows !== this.state.rows
);
if (sizeChanged) {
this.term.resize(this.state.cols, this.state.rows);
}
componentDidUpdate() {
if (!this.isEmbedded()) {
setDocumentTitle(this.getTitle());
}
@@ -240,34 +222,21 @@ class Terminal extends React.Component {
handleCloseClick(ev) {
ev.preventDefault();
this.props.dispatch(clickCloseTerminal(this.getPipeId()));
this.props.dispatch(closeTerminal(this.getPipeId()));
}
handlePopoutTerminal(ev) {
ev.preventDefault();
const paramString = JSON.stringify(this.props);
this.props.dispatch(clickCloseTerminal(this.getPipeId()));
this.props.dispatch(closeTerminal(this.getPipeId()));
this.setState({detached: true});
const bcr = this.node.getBoundingClientRect();
const minWidth = (this.state.characterWidth * 80) + (8 * 2);
openNewWindow(`${basePath(window.location.pathname)}/terminal.html#!/state/${paramString}`, bcr, minWidth);
openNewWindow(`${basePath(window.location.pathname)}/terminal.html#!/state/${paramString}`, bcr);
}
handleResize() {
// scrollbar === 16px
const width = this.innerFlex.clientWidth - (2 * 8) - 16;
const height = this.innerFlex.clientHeight - (2 * 8);
const cols = Math.floor(width / this.state.characterWidth);
const rows = Math.floor(height / this.state.characterHeight);
const resizeTtyControl = this.props.pipe.get('resizeTtyControl');
if (resizeTtyControl) {
doResizeTty(this.getPipeId(), resizeTtyControl, cols, rows)
.then(() => this.setState({cols, rows}));
} else if (!this.props.pipe.get('raw')) {
this.setState({cols, rows});
}
this.term.fit();
}
isEmbedded() {
@@ -298,9 +267,9 @@ class Terminal extends React.Component {
onClick={this.handlePopoutTerminal}>
Pop out
</span>
<span
<i
title="Close"
className="terminal-header-tools-item-icon fa fa-close"
className="terminal-header-tools-item-icon fa fa-times"
onClick={this.handleCloseClick} />
</div>
{this.getControlStatusIcon()}
@@ -310,18 +279,6 @@ class Terminal extends React.Component {
}
getStatus() {
if (this.props.pipe.get('status') === 'PIPE_DELETED') {
return (
<div>
<h3>Connection Closed</h3>
<div className="termina-status-bar-message">
The connection to this container has been closed.
<div className="link" onClick={this.handleCloseClick}>Close terminal</div>
</div>
</div>
);
}
if (!this.state.connected) {
return (
<h3>Connecting...</h3>
@@ -358,9 +315,6 @@ class Terminal extends React.Component {
opacity: this.state.connected ? 1 : 0.8,
overflow: 'hidden',
};
const innerStyle = {
width: (this.state.cols + 2) * this.state.characterWidth
};
const innerClassName = classNames('terminal-inner hideable', {
'terminal-inactive': !this.state.connected
});
@@ -368,9 +322,7 @@ class Terminal extends React.Component {
return (
<div className="terminal-wrapper" ref={this.saveNodeRef}>
{this.isEmbedded() && this.getTerminalHeader()}
<div className={innerClassName} style={innerFlexStyle} ref={this.saveInnerFlexRef}>
<div style={innerStyle} />
</div>
<div className={innerClassName} style={innerFlexStyle} ref={this.saveInnerFlexRef} />
{this.getTerminalStatusBar()}
</div>
);
@@ -378,7 +330,7 @@ class Terminal extends React.Component {
getControlStatusIcon() {
const icon = this.props.controlStatus && this.props.controlStatus.get('control').icon;
return (
<span
<i
style={{marginRight: '8px', width: '14px'}}
className={classNames('fa', {[icon]: icon})}
/>
+8 -27
View File
@@ -1,7 +1,7 @@
import React from 'react';
import moment from 'moment';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { TimestampTag } from 'weaveworks-ui-components';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import { pauseTimeAtNow, resumeTime } from '../actions/app-actions';
@@ -13,24 +13,7 @@ const className = isSelected => (
);
class TimeControl extends React.Component {
constructor(props, context) {
super(props, context);
this.handleNowClick = this.handleNowClick.bind(this);
this.handlePauseClick = this.handlePauseClick.bind(this);
this.getTrackingMetadata = this.getTrackingMetadata.bind(this);
}
componentDidMount() {
// Force periodic updates every one second for the paused info.
this.timer = setInterval(() => { this.forceUpdate(); }, 1000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
getTrackingMetadata(data = {}) {
getTrackingMetadata = (data = {}) => {
const { currentTopology } = this.props;
return {
layout: this.props.topologyViewMode,
@@ -40,12 +23,12 @@ class TimeControl extends React.Component {
};
}
handleNowClick() {
handleNowClick = () => {
trackAnalyticsEvent('scope.time.resume.click', this.getTrackingMetadata());
this.props.resumeTime();
}
handlePauseClick() {
handlePauseClick = () => {
trackAnalyticsEvent('scope.time.pause.click', this.getTrackingMetadata());
this.props.pauseTimeAtNow();
}
@@ -68,7 +51,7 @@ class TimeControl extends React.Component {
onClick={this.handleNowClick}
disabled={!topologiesLoaded}
title="Show live state of the system">
{!isPaused && <span className="fa fa-play" />}
{!isPaused && <i className="fa fa-play" />}
<span className="label">Live</span>
</span>
<span
@@ -76,16 +59,14 @@ class TimeControl extends React.Component {
onClick={this.handlePauseClick}
disabled={!topologiesLoaded}
title="Pause updates (freezes the nodes in their current layout)">
{isPaused && <span className="fa fa-pause" />}
{isPaused && <i className="fa fa-pause" />}
<span className="label">{isPaused ? 'Paused' : 'Pause'}</span>
</span>
</div>
</div>
{isPaused &&
<span
className="time-control-info"
title={moment(pausedAt).toISOString()}>
Showing state from {moment(pausedAt).fromNow()}
<span className="time-control-info">
Showing state from <TimestampTag inheritStyles relative timestamp={pausedAt} />
</span>
}
</div>
@@ -1,67 +1,11 @@
import React from 'react';
import moment from 'moment';
import { connect } from 'react-redux';
import { TimeTravel } from 'weaveworks-ui-components';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import { jumpToTime, resumeTime, pauseTimeAtNow } from '../actions/app-actions';
class TimeTravelWrapper extends React.Component {
constructor(props, context) {
super(props, context);
this.handleLiveModeChange = this.handleLiveModeChange.bind(this);
this.trackTimestampEdit = this.trackTimestampEdit.bind(this);
this.trackTimelinePanButtonClick = this.trackTimelinePanButtonClick.bind(this);
this.trackTimelineLabelClick = this.trackTimelineLabelClick.bind(this);
this.trackTimelineZoom = this.trackTimelineZoom.bind(this);
this.trackTimelinePan = this.trackTimelinePan.bind(this);
}
trackTimestampEdit() {
trackAnalyticsEvent('scope.time.timestamp.edit', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
}
trackTimelinePanButtonClick() {
trackAnalyticsEvent('scope.time.timeline.pan.button.click', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
}
trackTimelineLabelClick() {
trackAnalyticsEvent('scope.time.timeline.label.click', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
}
trackTimelinePan() {
trackAnalyticsEvent('scope.time.timeline.pan', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
}
trackTimelineZoom(zoomedPeriod) {
trackAnalyticsEvent('scope.time.timeline.zoom', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
zoomedPeriod,
});
}
handleLiveModeChange(showingLive) {
handleLiveModeChange = (showingLive) => {
if (showingLive) {
this.props.resumeTime();
} else {
@@ -71,47 +15,27 @@ class TimeTravelWrapper extends React.Component {
render() {
return (
<div className="tour-step-anchor time-travel-wrapper">
<TimeTravel
hasLiveMode
showingLive={this.props.showingLive}
onChangeLiveMode={this.handleLiveModeChange}
timestamp={this.props.timestamp}
earliestTimestamp={this.props.earliestTimestamp}
onChangeTimestamp={this.props.jumpToTime}
onTimestampInputEdit={this.trackTimestampEdit}
onTimelinePanButtonClick={this.trackTimelinePanButtonClick}
onTimelineLabelClick={this.trackTimelineLabelClick}
onTimelineZoom={this.trackTimelineZoom}
onTimelinePan={this.trackTimelinePan}
/>
</div>
<TimeTravel
hasLiveMode
timestamp={this.props.timestamp}
showingLive={this.props.showingLive}
onChangeTimestamp={this.props.jumpToTime}
onChangeLiveMode={this.handleLiveModeChange}
/>
);
}
}
function mapStateToProps(state, { params }) {
const scopeState = state.scope || state;
let firstSeenConnectedAt;
// If we're in the Weave Cloud context, use firstSeeConnectedAt as the earliest timestamp.
if (state.root && state.root.instances) {
const serviceInstance = state.root.instances[params && params.orgId];
if (serviceInstance && serviceInstance.firstSeenConnectedAt) {
firstSeenConnectedAt = moment(serviceInstance.firstSeenConnectedAt).utc().format();
}
}
function mapStateToProps(state) {
return {
showingLive: !scopeState.get('pausedAt'),
topologyViewMode: scopeState.get('topologyViewMode'),
currentTopology: scopeState.get('currentTopology'),
earliestTimestamp: firstSeenConnectedAt,
timestamp: scopeState.get('pausedAt'),
showingLive: !state.get('pausedAt'),
timestamp: state.get('pausedAt'),
};
}
export default connect(
mapStateToProps,
{ jumpToTime, resumeTime, pauseTimeAtNow },
{
jumpToTime, resumeTime, pauseTimeAtNow
},
)(TimeTravelWrapper);
@@ -6,6 +6,7 @@ import {
resetLocalViewState,
clickDownloadGraph
} from '../actions/app-actions';
import { getApiPath } from '../utils/web-api-utils';
class DebugMenu extends React.Component {
constructor(props, context) {
@@ -21,7 +22,7 @@ class DebugMenu extends React.Component {
render() {
const reportDownloadUrl = process.env.WEAVE_CLOUD
? `${window.location.origin}/api${window.location.pathname}/api/report`
? `${getApiPath()}/api/report`
: 'api/report';
return (
<div className="troubleshooting-menu-wrapper">
@@ -35,7 +36,7 @@ class DebugMenu extends React.Component {
download
title="Save raw data as JSON"
>
<span className="fa fa-code" />
<i className="fa fa-code" />
<span className="description">
Save raw data as JSON
</span>
@@ -47,7 +48,7 @@ class DebugMenu extends React.Component {
onClick={this.props.clickDownloadGraph}
title="Save canvas as SVG (does not include search highlighting)"
>
<span className="fa fa-download" />
<i className="fa fa-download" />
<span className="description">
Save canvas as SVG (does not include search highlighting)
</span>
@@ -59,7 +60,7 @@ class DebugMenu extends React.Component {
title="Reset view state"
onClick={this.handleClickReset}
>
<span className="fa fa-undo" />
<i className="fa fa-undo" />
<span className="description">Reset your local view state and reload the page</span>
</button>
</div>
@@ -71,14 +72,14 @@ class DebugMenu extends React.Component {
target="_blank"
rel="noopener noreferrer"
>
<span className="fa fa-bug" />
<i className="fa fa-bug" />
<span className="description">Report a bug</span>
</a>
</div>
<div className="help-panel-tools">
<span
<i
title="Close menu"
className="fa fa-close"
className="fa fa-times"
onClick={this.props.toggleTroubleshootingMenu}
/>
</div>
@@ -35,7 +35,7 @@ class ViewModeButton extends React.Component {
disabled={disabled}
onClick={!disabled ? this.handleClick : undefined}
title={`View ${label.toLowerCase()}`}>
<span className={this.props.icons} style={{ fontSize: 12 }} />
<i className={this.props.icons} />
<span className="label">{label}</span>
</div>
);
@@ -29,7 +29,7 @@ class ViewModeSelector extends React.Component {
<div className="tour-step-anchor view-mode-selector-wrapper">
<ViewModeButton
label="Graph"
icons="fa fa-share-alt"
icons="fa fa-sitemap"
viewMode={GRAPH_VIEW_MODE}
onClick={this.props.setGraphView}
/>
@@ -41,7 +41,7 @@ class ViewModeSelector extends React.Component {
/>
<ViewModeButton
label="Resources"
icons="fa fa-bar-chart"
icons="fa fa-chart-bar"
viewMode={RESOURCE_VIEW_MODE}
onClick={this.props.setResourceView}
disabled={!this.props.hasResourceView}
+1 -1
View File
@@ -27,7 +27,7 @@ class Warning extends React.Component {
return (
<div className={className} onClick={this.handleClick}>
<div className="warning-wrapper">
<span className="warning-icon fa fa-warning" title={text} />
<i className="warning-icon fa fa-exclamation-triangle" title={text} />
{expanded && <span className="warning-text">{text}</span>}
</div>
</div>
@@ -57,11 +57,11 @@ export default class ZoomControl extends React.Component {
return (
<div className="zoom-control">
<button className="zoom-in" onClick={this.handleZoomIn}>
<span className="fa fa-plus" />
<i className="fa fa-plus" />
</button>
<Slider value={value} max={1} step={SLIDER_STEP} vertical onChange={this.handleChange} />
<button className="zoom-out" onClick={this.handleZoomOut}>
<span className="fa fa-minus" />
<i className="fa fa-minus" />
</button>
</div>
);
+3 -7
View File
@@ -9,20 +9,19 @@ const ACTION_TYPES = [
'CLEAR_CONTROL_ERROR',
'CLICK_BACKGROUND',
'CLICK_CLOSE_DETAILS',
'CLICK_CLOSE_TERMINAL',
'CLICK_FORCE_RELAYOUT',
'CLICK_NODE',
'CLICK_RELATIVE',
'CLICK_SHOW_TOPOLOGY_FOR_NODE',
'CLICK_TERMINAL',
'CLICK_TOPOLOGY',
'CLOSE_TERMINAL',
'CLOSE_WEBSOCKET',
'DEBUG_TOOLBAR_INTERFERING',
'DESELECT_NODE',
'DO_CONTROL_ERROR',
'DO_CONTROL_SUCCESS',
'DO_CONTROL',
'DO_SEARCH',
'ENTER_EDGE',
'ENTER_NODE',
'FINISH_TIME_TRAVEL_TRANSITION',
@@ -37,7 +36,6 @@ const ACTION_TYPES = [
'PAUSE_TIME_AT_NOW',
'PIN_METRIC',
'PIN_NETWORK',
'PIN_SEARCH',
'RECEIVE_API_DETAILS',
'RECEIVE_CONTROL_NODE_REMOVED',
'RECEIVE_CONTROL_PIPE_STATUS',
@@ -48,28 +46,26 @@ const ACTION_TYPES = [
'RECEIVE_NODES_FOR_TOPOLOGY',
'RECEIVE_NODES',
'RECEIVE_NOT_FOUND',
'RECEIVE_SERVICE_IMAGES',
'RECEIVE_TOPOLOGIES',
'REQUEST_SERVICE_IMAGES',
'RESET_LOCAL_VIEW_STATE',
'RESUME_TIME',
'ROUTE_TOPOLOGY',
'SELECT_NETWORK',
'SET_EXPORTING_GRAPH',
'SET_RECEIVED_NODES_DELTA',
'SET_STORE_VIEW_STATE',
'SET_VIEW_MODE',
'SET_VIEWPORT_DIMENSIONS',
'SHOW_HELP',
'SHOW_NETWORKS',
'SHUTDOWN',
'SORT_ORDER_CHANGED',
'START_TIME_TRAVEL',
'TOGGLE_CONTRAST_MODE',
'TOGGLE_TROUBLESHOOTING_MENU',
'UNHOVER_METRIC',
'UNPIN_METRIC',
'UNPIN_NETWORK',
'UNPIN_SEARCH',
'UPDATE_SEARCH',
];
export default zipObject(ACTION_TYPES, ACTION_TYPES);
+14 -1
View File
@@ -22,6 +22,19 @@ export const UNIT_CLOUD_PATH = 'M-1.25 0.233Q-1.25 0.44-1.104 0.587-0.957 0.733-
+ '0.003-0.036 0.003-0.056 0-0.276-0.196-0.472-0.195-0.195-0.471-0.195-0.206 0-0.373 0.115-0.167'
+ ' 0.115-0.244 0.299-0.091-0.081-0.216-0.081-0.138 0-0.236 0.098-0.098 0.098-0.098 0.236 0 0.098'
+ ' 0.054 0.179-0.168 0.039-0.278 0.175-0.109 0.136-0.109 0.312z';
// Node Cylinder shape
export const UNIT_CYLINDER_PATH = 'm -1 -1.25' // this line is responsible for adjusting place of the shape with respect to dot
+ 'a 1 0.4 0 0 0 2 0'
+ 'm -2 0'
+ 'v 1.8'
+ 'a 1 0.4 0 0 0 2 0'
+ 'v -1.8'
+ 'a 1 0.4 0 0 0 -2 0';
// Node Storage Sheet Shape
export const SHEET = 'm -1.2 -1.6 m 0.4 0 v 2.4 m -0.4 -2.4 v 2.4 h 2 v -2.4 z m 0 0.4 h 2';
// NOTE: This value represents the node unit radius (in pixels). Since zooming is
// controlled at the top level now, this renormalization would be obsolete (i.e.
// value 1 could be used instead), if it wasn't for the following factors:
@@ -70,7 +83,7 @@ export const NODE_DETAILS_TABLE_COLUMN_WIDTHS = {
open_files_count: NODE_DETAILS_TABLE_CW.M,
pid: NODE_DETAILS_TABLE_CW.S,
port: NODE_DETAILS_TABLE_CW.S,
ppid: NODE_DETAILS_TABLE_CW.S,
ppid: NODE_DETAILS_TABLE_CW.M, // Label "Parent PID" needs more space
process_cpu_usage_percent: NODE_DETAILS_TABLE_CW.M,
process_memory_usage_bytes: NODE_DETAILS_TABLE_CW.M,
threads: NODE_DETAILS_TABLE_CW.M,
@@ -731,37 +731,4 @@ describe('RootReducer', () => {
constructEdgeId('def456', 'abc123')
]);
});
it('receives images for a service', () => {
const action = {
type: ActionTypes.RECEIVE_SERVICE_IMAGES,
serviceId: 'cortex/configs',
service: {
ID: 'cortex/configs',
Containers: [{
Available: [{
ID: 'quay.io/weaveworks/cortex-configs:master-1ca6274a',
CreatedAt: '2017-04-26T13:50:13.284736173Z'
}],
Current: { ID: 'quay.io/weaveworks/cortex-configs:master-1ca6274a' },
Name: 'configs'
}]
}
};
const nextState = reducer(initialState, action);
expect(nextState.getIn(['serviceImages', 'cortex/configs'])).toEqual({
isFetching: false,
errors: undefined,
containers: [{
Name: 'configs',
Current: {
ID: 'quay.io/weaveworks/cortex-configs:master-1ca6274a'
},
Available: [{
ID: 'quay.io/weaveworks/cortex-configs:master-1ca6274a',
CreatedAt: '2017-04-26T13:50:13.284736173Z'
}]
}]
});
});
});
+15 -42
View File
@@ -77,6 +77,7 @@ export const initialState = makeMap({
showingHelp: false,
showingTroubleshootingMenu: false,
showingNetworks: false,
storeViewState: true,
timeTravelTransitioning: false,
topologies: makeList(),
topologiesLoaded: false,
@@ -89,7 +90,6 @@ export const initialState = makeMap({
viewport: makeMap({ width: 0, height: 0 }),
websocketClosed: false,
zoomCache: makeMap(),
serviceImages: makeMap()
});
function calcSelectType(topology) {
@@ -219,6 +219,10 @@ export function rootReducer(state = initialState, action) {
return state.set('searchFocused', false);
}
case ActionTypes.FOCUS_SEARCH: {
return state.set('searchFocused', true);
}
case ActionTypes.CHANGE_TOPOLOGY_OPTION: {
// set option on parent topology
const topology = findTopologyById(state.get('topologies'), action.topologyId);
@@ -281,7 +285,7 @@ export function rootReducer(state = initialState, action) {
return closeNodeDetails(state, action.nodeId);
}
case ActionTypes.CLICK_CLOSE_TERMINAL: {
case ActionTypes.CLOSE_TERMINAL: {
return state.update('controlPipes', controlPipes => controlPipes.clear());
}
@@ -376,11 +380,6 @@ export function rootReducer(state = initialState, action) {
return state.set('pausedAt', moment().utc().format());
}
case ActionTypes.START_TIME_TRAVEL: {
state = state.set('timeTravelTransitioning', false);
return state.set('pausedAt', action.timestamp || moment().utc().format());
}
case ActionTypes.JUMP_TO_TIME: {
state = state.set('timeTravelTransitioning', true);
return state.set('pausedAt', action.timestamp);
@@ -471,10 +470,6 @@ export function rootReducer(state = initialState, action) {
}));
}
case ActionTypes.DO_SEARCH: {
return state.set('searchQuery', action.searchQuery);
}
case ActionTypes.ENTER_EDGE: {
return state.set('mouseOverEdgeId', action.edgeId);
}
@@ -504,14 +499,9 @@ export function rootReducer(state = initialState, action) {
}));
}
case ActionTypes.FOCUS_SEARCH: {
return state.set('searchFocused', true);
}
case ActionTypes.PIN_SEARCH: {
const pinnedSearches = state.get('pinnedSearches');
state = state.setIn(['pinnedSearches', pinnedSearches.size], action.query);
state = state.set('searchQuery', '');
case ActionTypes.UPDATE_SEARCH: {
state = state.set('pinnedSearches', makeList(action.pinnedSearches));
state = state.set('searchQuery', action.searchQuery || '');
return applyPinnedSearches(state);
}
@@ -688,7 +678,8 @@ export function rootReducer(state = initialState, action) {
pinnedMetricType: action.state.pinnedMetricType,
});
if (action.state.topologyOptions) {
state = state.set('topologyOptions', fromJS(action.state.topologyOptions));
const options = getDefaultTopologyOptions(state).mergeDeep(action.state.topologyOptions);
state = state.set('topologyOptions', options);
}
if (action.state.topologyViewMode) {
state = state.set('topologyViewMode', action.state.topologyViewMode);
@@ -726,12 +717,6 @@ export function rootReducer(state = initialState, action) {
return state;
}
case ActionTypes.UNPIN_SEARCH: {
const pinnedSearches = state.get('pinnedSearches').filter(query => query !== action.query);
state = state.set('pinnedSearches', pinnedSearches);
return applyPinnedSearches(state);
}
case ActionTypes.DEBUG_TOOLBAR_INTERFERING: {
return action.fn(state);
}
@@ -753,26 +738,14 @@ export function rootReducer(state = initialState, action) {
return clearNodes(state);
}
case ActionTypes.REQUEST_SERVICE_IMAGES: {
return state.setIn(['serviceImages', action.serviceId], {
isFetching: true
});
}
case ActionTypes.RECEIVE_SERVICE_IMAGES: {
const { service, errors, serviceId } = action;
return state.setIn(['serviceImages', serviceId], {
isFetching: false,
containers: service ? service.Containers : null,
errors
});
}
case ActionTypes.MONITOR_STATE: {
return state.set('monitor', action.monitor);
}
case ActionTypes.SET_STORE_VIEW_STATE: {
return state.set('storeViewState', action.storeViewState);
}
default: {
return state;
}
@@ -57,7 +57,8 @@ describe('WebApiUtils', () => {
});
it('returns the correct url when running as a component', () => {
process.env.SCOPE_API_PREFIX = '/api';
expect(getApiPath('/app/proud-cloud-77')).toEqual('/api/app/proud-cloud-77');
// instance ID first to match Weave Cloud routes
expect(getApiPath('/proud-cloud-77/app')).toEqual('/api/app/proud-cloud-77');
});
it('returns the correct url from an arbitrary path', () => {
expect(getApiPath('/demo/')).toEqual('/demo');
@@ -83,7 +84,7 @@ describe('WebApiUtils', () => {
});
it('returns the correct url when running as a component', () => {
process.env.SCOPE_API_PREFIX = '/api';
expect(getWebsocketUrl(host, '/app/proud-cloud-77')).toEqual(`ws://${host}/api/app/proud-cloud-77`);
expect(getWebsocketUrl(host, '/proud-cloud-77/app')).toEqual(`ws://${host}/api/app/proud-cloud-77`);
});
it('returns the correct url from an arbitrary path', () => {
expect(getWebsocketUrl(host, '/demo/')).toEqual(`ws://${host}/demo`);
+2 -2
View File
@@ -23,7 +23,7 @@ const loadScale = scaleLog().domain([0.01, 100]).range([0, 1]);
export function getMetricValue(metric) {
if (!metric) {
return {height: 0, value: null, formattedValue: 'n/a'};
return { height: 0, value: null, formattedValue: 'n/a' };
}
const m = metric.toJS();
const { value } = m;
@@ -35,7 +35,7 @@ export function getMetricValue(metric) {
max = null;
}
let displayedValue = Number(value).toFixed(1);
let displayedValue = Number(value);
if (displayedValue > 0 && (!max || displayedValue < max)) {
const baseline = 0.1;
displayedValue = (valuePercentage * (1 - (baseline * 2))) + baseline;
@@ -1,32 +0,0 @@
import React from 'react';
import range from 'lodash/range';
import { line, curveCardinalClosed } from 'd3-shape';
import { UNIT_CLOUD_PATH } from '../constants/styles';
export const pathElement = React.createFactory('path');
export const circleElement = React.createFactory('circle');
export const rectangleElement = React.createFactory('rect');
function curvedUnitPolygonPath(n) {
const curve = curveCardinalClosed.tension(0.65);
const spline = line().curve(curve);
const innerAngle = (2 * Math.PI) / n;
return spline(range(0, n).map(k => [
Math.sin(k * innerAngle),
-Math.cos(k * innerAngle),
]));
}
export const circleShapeProps = { r: 1 };
export const triangleShapeProps = { d: curvedUnitPolygonPath(3) };
export const squareShapeProps = {
width: 1.8, height: 1.8, rx: 0.4, ry: 0.4, x: -0.9, y: -0.9
};
export const pentagonShapeProps = { d: curvedUnitPolygonPath(5) };
export const hexagonShapeProps = { d: curvedUnitPolygonPath(6) };
export const heptagonShapeProps = { d: curvedUnitPolygonPath(7) };
export const octagonShapeProps = { d: curvedUnitPolygonPath(8) };
export const cloudShapeProps = { d: UNIT_CLOUD_PATH };
+51 -32
View File
@@ -1,4 +1,5 @@
import page from 'page';
import stableStringify from 'json-stable-stringify';
import { fromJS, is as isDeepEqual } from 'immutable';
import { each, omit, omitBy, isEmpty } from 'lodash';
@@ -36,6 +37,14 @@ export function parseHashState(hash = window.location.hash) {
return JSON.parse(decodeURL(urlStateString));
}
export function clearStoredViewState() {
storageSet(STORAGE_STATE_KEY, '');
}
function isStoreViewStateEnabled(state) {
return state.get('storeViewState');
}
function shouldReplaceState(prevState, nextState) {
// Opening a new terminal while an existing one is open.
const terminalToTerminal = (prevState.controlPipe && nextState.controlPipe);
@@ -107,12 +116,17 @@ export function getUrlState(state) {
export function updateRoute(getState) {
const state = getUrlState(getState());
const stateUrl = encodeURL(JSON.stringify(state));
const dispatch = false;
const prevState = parseHashState();
const dispatch = false;
const stateUrl = encodeURL(stableStringify(state));
const prevStateUrl = encodeURL(stableStringify(prevState));
if (stateUrl === prevStateUrl) return;
// back up state in storage as well
storageSet(STORAGE_STATE_KEY, stateUrl);
if (isStoreViewStateEnabled(getState())) {
storageSet(STORAGE_STATE_KEY, stateUrl);
}
if (shouldReplaceState(prevState, state)) {
// Replace the top of the history rather than pushing on a new item.
@@ -137,38 +151,43 @@ function detectOldOptions(topologyOptions) {
}
export function getRouter(dispatch, initialState) {
// strip any trailing '/'s.
page.base(window.location.pathname.replace(/\/$/, ''));
export function getRouter(initialState) {
return (dispatch, getState) => {
// strip any trailing '/'s.
page.base(window.location.pathname.replace(/\/$/, ''));
page('/', () => {
// recover from storage state on empty URL
const storageState = storageGet(STORAGE_STATE_KEY);
if (storageState) {
const parsedState = JSON.parse(decodeURL(storageState));
const dirtyOptions = detectOldOptions(parsedState.topologyOptions);
if (dirtyOptions) {
dispatch(route(initialState));
page('/', () => {
// recover from storage state on empty URL
const storageState = storageGet(STORAGE_STATE_KEY);
if (storageState && isStoreViewStateEnabled(getState())) {
const parsedState = JSON.parse(decodeURL(storageState));
const dirtyOptions = detectOldOptions(parsedState.topologyOptions);
if (dirtyOptions) {
dispatch(route(initialState));
} else {
const mergedState = Object.assign(initialState, parsedState);
// push storage state to URL
window.location.hash = `!/state/${stableStringify(mergedState)}`;
dispatch(route(mergedState));
}
} else {
const mergedState = Object.assign(initialState, parsedState);
// push storage state to URL
window.location.hash = `!/state/${JSON.stringify(mergedState)}`;
dispatch(route(mergedState));
dispatch(route(initialState));
}
} else {
dispatch(route(initialState));
}
});
});
page('/state/:state', (ctx) => {
const state = JSON.parse(decodeURL(ctx.params.state));
const dirtyOptions = detectOldOptions(state.topologyOptions);
if (dirtyOptions) {
dispatch(route(initialState));
} else {
dispatch(route(state));
}
});
page('/state/:state', (ctx) => {
const state = JSON.parse(decodeURL(ctx.params.state));
const dirtyOptions = detectOldOptions(state.topologyOptions);
const nextState = dirtyOptions ? initialState : state;
return page;
// back up state in storage and redirect
if (isStoreViewStateEnabled(getState())) {
storageSet(STORAGE_STATE_KEY, encodeURL(stableStringify(state)));
}
dispatch(route(nextState));
});
return page;
};
}
+37 -13
View File
@@ -2,30 +2,54 @@ import debug from 'debug';
const log = debug('scope:storage-utils');
// localStorage detection
const storage = (typeof Storage) !== 'undefined' ? window.localStorage : null;
export function storageGet(key, defaultValue) {
if (storage && storage.getItem(key) !== undefined) {
return storage.getItem(key);
export const localSessionStorage = {
getItem(k) {
return window.sessionStorage.getItem(k) || window.localStorage.getItem(k);
},
setItem(k, v) {
window.sessionStorage.setItem(k, v);
window.localStorage.setItem(k, v);
},
clear() {
window.sessionStorage.clear();
window.localStorage.clear();
}
return defaultValue;
};
export function storageGet(key, defaultValue, storage = localSessionStorage) {
if (!storage) {
return defaultValue;
}
const value = storage.getItem(key);
if (value == null) {
return defaultValue;
}
return value;
}
export function storageSet(key, value) {
export function storageSet(key, value, storage = localSessionStorage) {
if (storage) {
try {
storage.setItem(key, value);
return true;
} catch (e) {
log('Error storing value in storage. Maybe full? Could not store key.', key);
log(
'Error storing value in storage. Maybe full? Could not store key.',
key
);
}
}
return false;
}
export function storageGetObject(key, defaultValue) {
const value = storageGet(key);
export function storageGetObject(
key,
defaultValue,
storage = localSessionStorage
) {
const value = storageGet(key, undefined, storage);
if (value) {
try {
return JSON.parse(value);
@@ -36,9 +60,9 @@ export function storageGetObject(key, defaultValue) {
return defaultValue;
}
export function storageSetObject(key, obj) {
export function storageSetObject(key, obj, storage = localSessionStorage) {
try {
return storageSet(key, JSON.stringify(obj));
return storageSet(key, JSON.stringify(obj), storage);
} catch (e) {
log('Error encoding object for key', key);
}
+6 -3
View File
@@ -48,7 +48,6 @@ let createWebsocketAt = null;
let firstMessageOnWebsocketAt = null;
let continuePolling = true;
export function buildUrlQuery(params = makeMap(), state) {
// Attach the time travel timestamp to every request to the backend.
params = params.set('timestamp', state.get('pausedAt'));
@@ -88,9 +87,13 @@ export function basePathSlash(urlPath) {
return `${basePath(urlPath)}/`;
}
// TODO: This helper should probably be passed by the 'user' of Scope,
// i.e. in this case Weave Cloud, rather than being hardcoded here.
export function getApiPath(pathname = window.location.pathname) {
if (process.env.SCOPE_API_PREFIX) {
return basePath(`${process.env.SCOPE_API_PREFIX}${pathname}`);
// Extract the instance name (pathname in WC context is of format '/:instanceId/explore').
const instanceId = pathname.split('/')[1];
return basePath(`${process.env.SCOPE_API_PREFIX}/app/${instanceId}`);
}
return basePath(pathname);
@@ -104,7 +107,7 @@ function topologiesUrl(state) {
export function getWebsocketUrl(host = window.location.host, pathname = window.location.pathname) {
const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws';
return `${wsProto}://${host}${process.env.SCOPE_API_PREFIX || ''}${basePath(pathname)}`;
return `${wsProto}://${host}${getApiPath(pathname)}`;
}
function buildWebsocketUrl(topologyUrl, topologyOptions = makeMap(), state) {
File diff suppressed because it is too large Load Diff

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