mirror of
https://github.com/weaveworks/scope.git
synced 2026-09-06 10:17:19 +00:00
Merge branch 'master' into issues/3096
This commit is contained in:
@@ -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)
|
||||
@@ -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
|
||||
-->
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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) \
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
[](https://circleci.com/gh/weaveworks/scope/tree/master)
|
||||
[](https://coveralls.io/r/weaveworks/scope)
|
||||
[](https://goreportcard.com/report/github.com/weaveworks/scope)
|
||||
[](https://slack.weave.works)
|
||||
[](https://hub.docker.com/r/weaveworks/scope/)
|
||||
[](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).
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"context"
|
||||
|
||||
"github.com/weaveworks/common/mtime"
|
||||
"github.com/weaveworks/common/test"
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"context"
|
||||
|
||||
"github.com/weaveworks/scope/common/xfer"
|
||||
)
|
||||
|
||||
+2
-2
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
consul "github.com/hashicorp/consul/api"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"context"
|
||||
|
||||
"github.com/weaveworks/scope/app"
|
||||
)
|
||||
|
||||
+2
-2
@@ -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
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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,5 +1,6 @@
|
||||
node_modules
|
||||
build/
|
||||
build-external/
|
||||
coverage/
|
||||
test/*png
|
||||
weave-scope.tgz
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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.
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -15,6 +15,7 @@ class Details extends React.Component {
|
||||
index={index}
|
||||
cardCount={details.size}
|
||||
nodeControlStatus={controlStatus.get(obj.id)}
|
||||
renderNodeDetailsExtras={this.props.renderNodeDetailsExtras}
|
||||
{...obj}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
@@ -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) :
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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})}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
}]
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+174
-553
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
Reference in New Issue
Block a user