commit cda7c1a4748395648fe6b305d92443e17eaee700 Author: upstream-mirror Date: Wed Sep 16 05:24:45 2026 +0500 snapshot of https://github.com/traggo/server.git (master, 2026-09-16; blobs > 131072B omitted) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..934a9c7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +/docker/Dockerfile.dev +/ui/node_modules/ +.idea/ +build/ diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..179b7c6 --- /dev/null +++ b/.env.development @@ -0,0 +1,3 @@ +TRAGGO_LOG_LEVEL=debug +TRAGGO_DATABASE_DIALECT=sqlite3 +TRAGGO_DATABASE_CONNECTION=file::memory:?mode=memory&cache=shared \ No newline at end of file diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..3140142 --- /dev/null +++ b/.env.sample @@ -0,0 +1,19 @@ +# the port the http server should use +# TRAGGO_PORT=3030 + +# default username and password +# TRAGGO_DEFAULT_USER_NAME=admin +# TRAGGO_DEFAULT_USER_PASS=admin + +# bcrypt password strength (higher = more secure but also slower) +# TRAGGO_PASS_STRENGTH=10 + +# how verbose traggo/server should log (must be one of: debug, info, warn, error, fatal, panic) +# TRAGGO_LOG_LEVEL=info + +# the database dialect (must be one of: sqlite3) +# TRAGGO_DATABASE_DIALECT=sqlite3 + +# the database connection string, differs depending on the dialect +# sqlite3: path/to/database.db +# TRAGGO_DATABASE_CONNECTION=data/traggo.db diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..4e7263b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: jmattheis +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: https://jmattheis.de/donate diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..97bc07e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: a:bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. (Include configs for traggo and reverse proxies like nginx) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..a6e817a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: a:feature +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/questions.md b/.github/ISSUE_TEMPLATE/questions.md new file mode 100644 index 0000000..2a65484 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/questions.md @@ -0,0 +1,29 @@ +--- +name: Questions +about: Having difficulties with traggo? +title: '' +labels: question +assignees: '' + +--- + + + +**Have you read the documentation?** +- [ ] Yes, but it does not include related information regarding my question. +- [ ] Yes, but the steps described in the documentation do not work on my machine. +- [ ] Yes, but I am having difficulty understanding it and want clarification. + +**You are setting up traggo in** +- [ ] Docker +- [ ] Linux native platform +- [ ] Windows native platform + +**Describe your problem** +A clear and concise description of what the question is. + +**Any errors, logs, or other information that might help us identify your problem** + +Ex: `docker-compose.yml`, `nginx.conf`, browser requests, etc. diff --git a/.github/traggo_calendar.png b/.github/traggo_calendar.png new file mode 100644 index 0000000..360f7a5 Binary files /dev/null and b/.github/traggo_calendar.png differ diff --git a/.github/traggo_dashboard.png b/.github/traggo_dashboard.png new file mode 100644 index 0000000..3e5962f Binary files /dev/null and b/.github/traggo_dashboard.png differ diff --git a/.github/traggo_list.png b/.github/traggo_list.png new file mode 100644 index 0000000..57efb7e Binary files /dev/null and b/.github/traggo_list.png differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..89faa2f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: build +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v6 + with: + go-version: 1.25.x + - uses: actions/setup-node@v6 + with: + node-version: '20' + - uses: actions/checkout@v5 + - run: make download-tools + - run: make install + - run: make generate + - run: make build-js + env: + NODE_OPTIONS: --openssl-legacy-provider + - run: make lint + - run: make test + - if: startsWith(github.ref, 'refs/tags/v') + run: echo "$DOCKER_PASS" | docker login --username "traggoci" --password-stdin + env: + DOCKER_PASS: ${{ secrets.DOCKER_PASS }} + - if: startsWith(github.ref, 'refs/tags/v') + run: make release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b233311 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.idea +dist/ +build +vendor +coverage.txt +generated +.env.development.local +.env +npm-debug.log* +yarn-debug.log* +yarn-error.log* +node_modules +__generated__ +*-packr.go diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..7a48a93 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,96 @@ +version: 2 +project_name: traggo +builds: + - id: linux-amd64 + main: ./ + goos: [linux] + goarch: ['amd64'] + flags: ['-tags="netgo osusergo sqlite_omit_load_extension"'] + ldflags: ["-s","-w","-linkmode","external","-extldflags","-static","-X main.BuildDate={{.Date}}","-X main.BuildMode=prod","-X main.BuildCommit={{.Commit}}","-X main.BuildVersion={{.Version}}"] + - id: linux-armhf + main: ./ + goos: [linux] + goarch: [arm] + goarm: [7] + env: [CC=arm-linux-gnueabihf-gcc, CXX=arm-linux-gnueabihf-g++] + flags: ['-tags="netgo osusergo sqlite_omit_load_extension"'] + ldflags: ["-s","-w","-linkmode","external","-extldflags","-static","-X main.BuildDate={{.Date}}","-X main.BuildMode=prod","-X main.BuildCommit={{.Commit}}","-X main.BuildVersion={{.Version}}"] + - id: linux-arm64 + main: ./ + goos: [linux] + goarch: [arm64] + env: [CC=aarch64-linux-gnu-gcc, CXX=aarch64-linux-gnu-g++] + flags: ['-tags="netgo osusergo sqlite_omit_load_extension"'] + ldflags: ["-s","-w","-linkmode","external","-extldflags","-static","-X main.BuildDate={{.Date}}","-X main.BuildMode=prod","-X main.BuildCommit={{.Commit}}","-X main.BuildVersion={{.Version}}"] + - id: windows-amd64 + main: ./ + goos: [windows] + goarch: [amd64] + env: [CC=x86_64-w64-mingw32-gcc, CXX=x86_64-w64-mingw32-gcc] + flags: ['-tags="netgo osusergo sqlite_omit_load_extension"'] + ldflags: ["-s","-w","-linkmode","external","-extldflags","-static","-X main.BuildDate={{.Date}}","-X main.BuildMode=prod","-X main.BuildCommit={{.Commit}}","-X main.BuildVersion={{.Version}}"] +archives: + - files: + - LICENSE + - README.md + - .env.sample + name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{- if eq .Arch "386" }}i386{{- else }}{{ .Arch }}{{ end }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + format_overrides: + - goos: windows + formats: [zip] +checksum: + disable: true +changelog: + disable: true +dockers: + - use: buildx + goos: linux + goarch: amd64 + image_templates: + - "traggo/server:amd64-latest" + - "traggo/server:amd64-{{ .RawVersion }}" + dockerfile: docker/Dockerfile + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - use: buildx + goos: linux + goarch: arm64 + image_templates: + - "traggo/server:arm64-latest" + - "traggo/server:arm64-{{ .RawVersion }}" + dockerfile: docker/Dockerfile + build_flag_templates: + - "--platform=linux/arm64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - use: buildx + goos: linux + goarch: arm + goarm: 7 + image_templates: + - "traggo/server:armv7-latest" + - "traggo/server:armv7-{{ .RawVersion }}" + dockerfile: docker/Dockerfile + build_flag_templates: + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" +docker_manifests: + - name_template: "traggo/server:latest" + image_templates: + - "traggo/server:amd64-latest" + - "traggo/server:arm64-latest" + - "traggo/server:armv7-latest" + - name_template: "traggo/server:{{ .RawVersion }}" + image_templates: + - "traggo/server:amd64-{{ .RawVersion }}" + - "traggo/server:arm64-{{ .RawVersion }}" + - "traggo/server:armv7-{{ .RawVersion }}" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..193b2d6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at traggo@protonmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..19bd886 --- /dev/null +++ b/Makefile @@ -0,0 +1,94 @@ +TAGS=netgo osusergo sqlite_omit_load_extension +VERSION=$(shell git describe --tags --abbrev=0 | cut -c 2-) +COMMIT=$(shell git rev-parse --verify HEAD) +DATE=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") +LD_FLAGS=-s -w -linkmode external -extldflags "-static" -X main.BuildDate=$(DATE) -X main.BuildMode=prod -X main.BuildCommit=$(COMMIT) -X main.BuildVersion=$(VERSION) +BUILD_DIR=./build +PWD=$(shell pwd) + +license-dir: + mkdir -p build/license || true + +download-tools: + go install golang.org/x/tools/cmd/goimports@v0.1.10 + go install github.com/99designs/gqlgen@v0.17.85 + +generate-go: + gqlgen + +generate-js: + (cd ui && yarn generate) + +generate: generate-go generate-js + +lint-go: + go vet ./... + goimports -l $(shell find . -type f -name '*.go' -not -path "./vendor/*") + +lint-js: + (cd ui && yarn format:check) + (cd ui && yarn lint:check) + +lint: lint-go lint-js + +format-go: + goimports -w $(shell find . -type f -name '*.go' -not -path "./vendor/*") + +format-js: + (cd ui && yarn format) + +format: format-go format-js + +test-go: + go test --race -coverprofile=coverage.txt -covermode=atomic ./... + +test-js: + (cd ui && CI=true yarn test) + +test: test-go test-js + +install-go: + go mod download + +install-js: + (cd ui && yarn) + +build-js: + (cd ui && yarn build) + +pre-build: build-js + +build-bin-local: pre-build + CGO_ENABLED=1 go build -a -ldflags '${LD_FLAGS}' -tags '${TAGS}' -o ${BUILD_DIR}/traggo-server + +.PHONY: release +release: + docker build -t traggo:build -f docker/Dockerfile.build docker + docker run \ + --rm \ + -v "$$HOME/.docker/config.json:/root/.docker/config.json" \ + -e CGO_ENABLED=1 \ + -e GITHUB_TOKEN="$$GITHUB_TOKEN" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $$PWD:/work \ + -w /work \ + traggo:build \ + release --skip=validate --clean + +.PHONY: release-snapshot +release-snapshot: + docker build -t traggo:build -f docker/Dockerfile.build docker + docker run \ + --rm \ + -v "$$HOME/.docker/config.json:/root/.docker/config.json" \ + -e CGO_ENABLED=1 \ + -e GITHUB_TOKEN="$$GITHUB_TOKEN" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $$PWD:/work \ + -w /work \ + traggo:build \ + release --clean --snapshot + +install: install-go install-js + +.PHONY: build diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c2e9be --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +

+ + + +

+ +

traggo/server

+

tag-based time tracking

+ +

+ + Build Status + + + codecov + + + Docker Pulls + + + latest release + +

+ + +Traggo is a tag-based time tracking tool. In Traggo there are no tasks, only [tagged](https://traggo.net/terminology/#tag) [time spans](https://traggo.net/terminology/#timespan). + +With [tags](https://traggo.net/terminology/#tag), Traggo tries to be as customizable as possible, f.ex. if you work on different projects you could add a `project`-tag. +If you like to see statistics from the different things you do, you could add a `type`-tag with values like `email`, `programming`, `meeting`. +You can do it just as you like. + +If you want to use Traggo, you need to host it yourself. This way, you have the full control over your data and no third-party +may be able to read it. Have a look at our [Install Guide](https://traggo.net/install/). + +## Features + +* easy to setup +* time tracking (obviously) +* customizable dashboards with diagrams +* a list and calendar view of the tracked time +* sleek web ui with multiple themes +* simple user management + +--- + + + traggo list + + + traggo list + + + traggo list + + +[Install](https://traggo.net/install/) ᛫ +[Configuration](https://traggo.net/config/) ᛫ +[Setup Dev Environment](https://traggo.net/dev/setup/) + +## Versioning +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the +[tags on this repository](https://github.com/traggo/server/tags). diff --git a/auth/cleanup.go b/auth/cleanup.go new file mode 100644 index 0000000..d16971a --- /dev/null +++ b/auth/cleanup.go @@ -0,0 +1,29 @@ +package auth + +import ( + "time" + + "github.com/jinzhu/gorm" + "github.com/jmattheis/go-timemath" + "github.com/rs/zerolog/log" + "github.com/traggo/server/model" +) + +var timeNow = time.Now + +// CleanUp clean up expired devices +func CleanUp(db *gorm.DB, interval time.Duration, close chan bool) { + for { + select { + case <-time.After(interval): + affected := db.Where("type = ? AND active_at < ?", model.TypeLongExpiry, timemath.Second.Subtract(timeNow(), model.TypeLongExpiry.Seconds())). + Or("type = ? AND active_at < ?", model.TypeShortExpiry, timemath.Second.Subtract(timeNow(), model.TypeShortExpiry.Seconds())). + Delete(new(model.Device)).RowsAffected + if affected > 0 { + log.Debug().Int64("amount", affected).Msg("removed devices") + } + case <-close: + return + } + } +} diff --git a/auth/cleanup_test.go b/auth/cleanup_test.go new file mode 100644 index 0000000..f96e7ec --- /dev/null +++ b/auth/cleanup_test.go @@ -0,0 +1,138 @@ +package auth + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +func TestCleanUp_stops(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + methodDone := make(chan struct{}) + stopCleanUp := make(chan bool) + + go func() { + CleanUp(db.DB, time.Microsecond*10, stopCleanUp) + methodDone <- struct{}{} + }() + + stopCleanUp <- true + + select { + case <-methodDone: + // expected + case <-time.After(time.Second): + t.Fail() + } +} + +func TestCleanUp_removeExpiredDevices(t *testing.T) { + now := test.Time("2018-06-30T18:30:00Z") + timeDispose := fakeTime(now) + defer timeDispose() + + db := test.InMemoryDB(t) + defer db.Close() + db.User(2) + db.Create(&model.Device{ + ID: 1, + Token: "abc", + UserID: 2, + Name: "android 1", + Type: model.TypeShortExpiry, + ActiveAt: test.Time("2018-06-30T17:30:00Z"), + CreatedAt: test.Time("2009-06-30T18:30:00Z"), + }) + db.Create(&model.Device{ + ID: 2, + Token: "abc2", + UserID: 2, + Name: "android 2", + Type: model.TypeShortExpiry, + ActiveAt: test.Time("2018-06-30T17:29:59Z"), + CreatedAt: test.Time("2009-06-30T18:30:00Z"), + }) + db.Create(&model.Device{ + ID: 3, + Token: "abc3", + UserID: 2, + Name: "android 3", + Type: model.TypeLongExpiry, + ActiveAt: test.Time("2018-05-31T18:30:00Z"), + CreatedAt: test.Time("2009-06-30T18:30:00Z"), + }) + db.Create(&model.Device{ + ID: 4, + Token: "abc4", + UserID: 2, + Name: "android 4", + Type: model.TypeLongExpiry, + ActiveAt: test.Time("2018-05-31T18:29:59Z"), + CreatedAt: test.Time("2009-06-30T18:30:00Z"), + }) + db.Create(&model.Device{ + ID: 5, + Token: "abc5", + UserID: 2, + Name: "android 5", + Type: model.TypeNoExpiry, + ActiveAt: test.Time("2002-06-30T18:30:00Z"), + CreatedAt: test.Time("2000-06-30T18:30:00Z"), + }) + + stopCleanUp := make(chan bool) + + go CleanUp(db.DB, time.Microsecond*10, stopCleanUp) + + <-time.After(time.Millisecond * 20) + + expected := []model.Device{ + { + ID: 1, + Token: "abc", + UserID: 2, + Name: "android 1", + Type: model.TypeShortExpiry, + ActiveAt: test.Time("2018-06-30T17:30:00Z"), + CreatedAt: test.Time("2009-06-30T18:30:00Z"), + }, + { + ID: 3, + Token: "abc3", + UserID: 2, + Name: "android 3", + Type: model.TypeLongExpiry, + ActiveAt: test.Time("2018-05-31T18:30:00Z"), + CreatedAt: test.Time("2009-06-30T18:30:00Z"), + }, + { + ID: 5, + Token: "abc5", + UserID: 2, + Name: "android 5", + Type: model.TypeNoExpiry, + ActiveAt: test.Time("2002-06-30T18:30:00Z"), + CreatedAt: test.Time("2000-06-30T18:30:00Z"), + }, + } + + var devices []model.Device + db.Find(&devices) + assert.Equal(t, expected, devices) + stopCleanUp <- true +} + +func fakeTime(t time.Time) func() { + old := timeNow + timeNow = func() time.Time { + return t + } + return func() { + timeNow = old + } +} diff --git a/auth/hasrole.go b/auth/hasrole.go new file mode 100644 index 0000000..f7e6118 --- /dev/null +++ b/auth/hasrole.go @@ -0,0 +1,26 @@ +package auth + +import ( + "context" + "errors" + + "github.com/99designs/gqlgen/graphql" + "github.com/traggo/server/generated/gqlmodel" +) + +// HasRole checks if the current user has sufficient permissions. +func HasRole() func(ctx context.Context, obj interface{}, next graphql.Resolver, role gqlmodel.Role) (res interface{}, err error) { + return func(ctx context.Context, obj interface{}, next graphql.Resolver, role gqlmodel.Role) (interface{}, error) { + user := GetUser(ctx) + + if user == nil { + return nil, errors.New("you need to login") + } + + if role == gqlmodel.RoleAdmin && !user.Admin { + return nil, errors.New("permission denied") + } + + return next(ctx) + } +} diff --git a/auth/hasrole_test.go b/auth/hasrole_test.go new file mode 100644 index 0000000..47bfd51 --- /dev/null +++ b/auth/hasrole_test.go @@ -0,0 +1,55 @@ +package auth_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/test/fake" +) + +var ( + successResp = struct{}{} +) + +func TestHasRole_requiredUser_givenAdmin_succeeds(t *testing.T) { + res, err := auth.HasRole()(fake.UserWithPerm(1, true), nil, noop, gqlmodel.RoleUser) + assert.Nil(t, err) + assert.Equal(t, successResp, res) +} + +func TestHasRole_requiredAdmin_givenAdmin_succeeds(t *testing.T) { + res, err := auth.HasRole()(fake.UserWithPerm(1, true), nil, noop, gqlmodel.RoleAdmin) + assert.Nil(t, err) + assert.Equal(t, successResp, res) +} + +func TestHasRole_requiredUser_givenUser_succeeds(t *testing.T) { + res, err := auth.HasRole()(fake.UserWithPerm(1, false), nil, noop, gqlmodel.RoleUser) + assert.Nil(t, err) + assert.Equal(t, successResp, res) +} + +func TestHasRole_requiredAdmin_givenUser_fails(t *testing.T) { + res, err := auth.HasRole()(fake.UserWithPerm(1, false), nil, noop, gqlmodel.RoleAdmin) + assert.Nil(t, res) + assert.EqualError(t, err, "permission denied") +} + +func TestHasRole_requiredAdmin_noUserGiven_fails(t *testing.T) { + res, err := auth.HasRole()(context.Background(), nil, noop, gqlmodel.RoleAdmin) + assert.Nil(t, res) + assert.EqualError(t, err, "you need to login") +} + +func TestHasRole_requiredUser_noUserGiven_fails(t *testing.T) { + res, err := auth.HasRole()(context.Background(), nil, noop, gqlmodel.RoleUser) + assert.Nil(t, res) + assert.EqualError(t, err, "you need to login") +} + +func noop(context.Context) (res interface{}, err error) { + return successResp, nil +} diff --git a/auth/middleware.go b/auth/middleware.go new file mode 100644 index 0000000..e68a769 --- /dev/null +++ b/auth/middleware.go @@ -0,0 +1,182 @@ +package auth + +import ( + "context" + "errors" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/jinzhu/gorm" + "github.com/rs/zerolog/log" + "github.com/traggo/server/model" +) + +type key string + +var ( + traggoSession = "traggo" + createSessionKey key = "createSession" + destroySessionKey key = "destroySession" + userKey key = "user" + deviceKey key = "device" +) + +// Middleware is the auth middleware which sets user and device context parameters. +func Middleware(db *gorm.DB) mux.MiddlewareFunc { + return func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + handler.ServeHTTP(writer, sessionCallbacks(reqisterUser(request, writer, db), writer)) + }) + } +} + +func sessionCallbacks(request *http.Request, writer http.ResponseWriter) *http.Request { + createSession := func(token string, maxAge int) { + http.SetCookie(writer, &http.Cookie{ + Name: traggoSession, + Value: token, + MaxAge: maxAge, + }) + } + destroySession := func() { + http.SetCookie(writer, &http.Cookie{ + Name: traggoSession, + Value: "", + MaxAge: -1, + }) + } + return request.WithContext(WithDestroySession(WithCreateSession(request.Context(), createSession), destroySession)) +} + +func reqisterUser(request *http.Request, writer http.ResponseWriter, db *gorm.DB) *http.Request { + token, err := getToken(request) + if err != nil { + return request + } + + device := &model.Device{} + if db.Where("token = ?", token).Find(device).RecordNotFound() { + log.Debug().Str("token", token).Msg("No device with token found") + return request + } + + user := &model.User{} + if db.Find(user, device.UserID).RecordNotFound() { + log.Panic().Int("userID", device.UserID).Int("deviceID", device.ID).Msg("User not found") + } + log.Debug().Int("userid", device.UserID).Str("username", user.Name).Msg("User found") + + impersonate := request.Header.Get("X-Traggo-Impersonate") + if impersonate != "" { + if !user.Admin { + log.Info().Str("impersonate", impersonate).Msg("Trying to impersonate without being admin") + return request + } + userID, err := strconv.Atoi(impersonate) + if err != nil { + log.Info().Str("impersonate", impersonate).Msg("Unable to parse impersonation header") + return request + } + impersonateUser := &model.User{} + if db.Find(impersonateUser, userID).RecordNotFound() { + log.Info().Int("userid", userID).Msg("Impersonation user not found") + return request + } + user = impersonateUser + log.Info().Str("username", user.Name).Msg("Impersonation user") + } + + if device.ActiveAt.Before(time.Now().Add(5 * -time.Minute)) { + log.Debug().Int("deviceId", device.ID).Str("deviceName", device.Name).Msg("update device activeAt") + device.ActiveAt = timeNow() + db.Save(device) + + if cookie, err := request.Cookie(traggoSession); err == nil && cookie != nil { + cookie.MaxAge = device.Type.Seconds() + http.SetCookie(writer, cookie) + } + } + + return request.WithContext(WithUser(WithDevice(request.Context(), device), user)) +} + +func getToken(request *http.Request) (string, error) { + if value := request.Header.Get("Authorization"); value != "" && strings.HasPrefix(value, "traggo ") { + log.Debug().Msg("Using authorization header for authentication") + key := strings.TrimPrefix(value, "traggo ") + return key, nil + } + + if cookie, err := request.Cookie("traggo"); err == nil && cookie != nil && cookie.Value != "" { + log.Debug().Msg("Using cookie for authentication") + return cookie.Value, nil + } + + if token := request.FormValue("token"); token != "" { + log.Debug().Msg("Using query parameter for authentication") + return token, nil + } + + return "", errors.New("no token found") +} + +// WithDevice adds the authenticated device to the context. +func WithDevice(ctx context.Context, device *model.Device) context.Context { + return context.WithValue( + ctx, + deviceKey, + device) +} + +// WithUser adds the authenticated user to the context. +func WithUser(ctx context.Context, user *model.User) context.Context { + return context.WithValue( + ctx, + userKey, + user) +} + +// WithCreateSession adds the create session method +func WithCreateSession(ctx context.Context, f func(token string, age int)) context.Context { + return context.WithValue(ctx, createSessionKey, f) +} + +// WithDestroySession adds the destroy session method +func WithDestroySession(ctx context.Context, f func()) context.Context { + return context.WithValue(ctx, destroySessionKey, f) +} + +// GetUser returns the authenticated user or panics. +func GetUser(ctx context.Context) *model.User { + if user, ok := ctx.Value(userKey).(*model.User); ok { + return user + } + return nil +} + +// GetDevice returns the authenticated device or nil. +func GetDevice(ctx context.Context) *model.Device { + if device, ok := ctx.Value(deviceKey).(*model.Device); ok { + return device + } + return nil +} + +// GetCreateSession gets the create session callback +func GetCreateSession(ctx context.Context) func(token string, age int) { + if f, ok := ctx.Value(createSessionKey).(func(string, int)); ok { + return f + } + panic("create session must exist") +} + +// GetDestroySession gets the destroy session callback +func GetDestroySession(ctx context.Context) func() { + if f, ok := ctx.Value(destroySessionKey).(func()); ok { + return f + } + panic("create session must exist") +} diff --git a/auth/middleware_test.go b/auth/middleware_test.go new file mode 100644 index 0000000..97f1977 --- /dev/null +++ b/auth/middleware_test.go @@ -0,0 +1,328 @@ +package auth + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/traggo/server/test" +) + +func TestMiddleware_noAuthentication_noAuthentication(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + spy := &requestSpy{} + + Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/test", nil)) + + ctx := spy.req.Context() + assert.Nil(t, GetUser(ctx)) + assert.Nil(t, GetDevice(ctx)) +} + +func TestMiddleware_query_notExistingToken_noAuthentication(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + spy := &requestSpy{} + + Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/test?token=abc", nil)) + + ctx := spy.req.Context() + assert.Nil(t, GetUser(ctx)) + assert.Nil(t, GetDevice(ctx)) +} + +func TestMiddleware_query_validToken_authenticates(t *testing.T) { + now := test.Time("2018-06-30T18:30:00Z") + timeDispose := fakeTime(now) + defer timeDispose() + + db := test.InMemoryDB(t) + defer db.Close() + builder := db.User(1) + user := builder.User + device := builder.NewDevice(2, "abc", "test") + spy := &requestSpy{} + + Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/test?token=abc", nil)) + + ctx := spy.req.Context() + assert.Equal(t, &user, GetUser(ctx)) + + device.ActiveAt = now + assert.Equal(t, &device, GetDevice(ctx)) +} + +func TestMiddleware_header_notExistingToken_noAuthentication(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + spy := &requestSpy{} + + request := httptest.NewRequest("GET", "/test?token=abc", nil) + request.Header["Authorization"] = []string{"traggo abc"} + Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request) + + ctx := spy.req.Context() + assert.Nil(t, GetUser(ctx)) + assert.Nil(t, GetDevice(ctx)) +} + +func TestMiddleware_header_validToken_authenticates(t *testing.T) { + now := test.Time("2018-06-30T18:30:00Z") + timeDispose := fakeTime(now) + defer timeDispose() + + db := test.InMemoryDB(t) + defer db.Close() + builder := db.User(1) + user := builder.User + device := builder.NewDevice(2, "abc", "test") + spy := &requestSpy{} + + request := httptest.NewRequest("GET", "/test", nil) + request.Header.Set("Authorization", "traggo abc") + Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request) + + ctx := spy.req.Context() + assert.Equal(t, &user, GetUser(ctx)) + + device.ActiveAt = now + assert.Equal(t, &device, GetDevice(ctx)) +} + +func TestMiddleware_header_validToken_invalidAuthenticationType_noAuthentication(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(1).NewDevice(2, "abc", "test") + spy := &requestSpy{} + + request := httptest.NewRequest("GET", "/test", nil) + request.Header.Set("Authorization", "basic abc") + + Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request) + + ctx := spy.req.Context() + assert.Nil(t, GetUser(ctx)) + assert.Nil(t, GetDevice(ctx)) +} + +func TestMiddleware_cookie_notExistingToken_noAuthentication(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + spy := &requestSpy{} + + request := httptest.NewRequest("GET", "/test?token=abc", nil) + request.AddCookie(&http.Cookie{ + Name: "traggo", + Value: "abc", + }) + Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request) + + ctx := spy.req.Context() + assert.Nil(t, GetUser(ctx)) + assert.Nil(t, GetDevice(ctx)) +} + +func TestMiddleware_cookie_validToken_authenticates(t *testing.T) { + now := test.Time("2018-06-30T18:30:00Z") + timeDispose := fakeTime(now) + defer timeDispose() + + db := test.InMemoryDB(t) + defer db.Close() + builder := db.User(1) + user := builder.User + device := builder.NewDevice(2, "abc", "test") + spy := &requestSpy{} + + request := httptest.NewRequest("GET", "/test?token=abc", nil) + request.AddCookie(&http.Cookie{ + Name: "traggo", + Value: "abc", + }) + Middleware(db.DB)(spy).ServeHTTP(httptest.NewRecorder(), request) + + ctx := spy.req.Context() + assert.Equal(t, &user, GetUser(ctx)) + + device.ActiveAt = now + assert.Equal(t, &device, GetDevice(ctx)) +} + +func TestMiddleware_createSession_setsCookie(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + spy := &requestSpy{} + recorder := httptest.NewRecorder() + + request := httptest.NewRequest("GET", "/test", nil) + Middleware(db.DB)(spy).ServeHTTP(recorder, request) + + ctx := spy.req.Context() + GetCreateSession(ctx)("new token", 60) + + cookieHeader := recorder.Header().Get("Set-Cookie") + assert.Equal(t, `traggo="new token"; Max-Age=60`, cookieHeader) +} + +func TestMiddleware_noCallbackExecuted_noCookieSet(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + spy := &requestSpy{} + recorder := httptest.NewRecorder() + + request := httptest.NewRequest("GET", "/test", nil) + Middleware(db.DB)(spy).ServeHTTP(recorder, request) + + cookieHeader := recorder.Header().Get("Set-Cookie") + assert.Equal(t, "", cookieHeader) +} + +func TestMiddleware_destroySession_destroysCookie(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + spy := &requestSpy{} + recorder := httptest.NewRecorder() + + request := httptest.NewRequest("GET", "/test", nil) + Middleware(db.DB)(spy).ServeHTTP(recorder, request) + + ctx := spy.req.Context() + GetDestroySession(ctx)() + + cookieHeader := recorder.Header().Get("Set-Cookie") + assert.Equal(t, "traggo=; Max-Age=0", cookieHeader) +} + +func TestMiddleware_impersonate_no_admin(t *testing.T) { + now := test.Time("2018-06-30T18:30:00Z") + timeDispose := fakeTime(now) + defer timeDispose() + + db := test.InMemoryDB(t) + defer db.Close() + builder := db.User(1) + builder.NewDevice(2, "abc", "test") + spy := &requestSpy{} + recorder := httptest.NewRecorder() + + request := httptest.NewRequest("GET", "/test?token=abc", nil) + request.Header.Set("X-Traggo-Impersonate", "empty") + Middleware(db.DB)(spy).ServeHTTP(recorder, request) + + response := recorder.Result() + assert.Equal(t, 200, response.StatusCode) + + ctx := spy.req.Context() + assert.Nil(t, GetUser(ctx)) + assert.Nil(t, GetDevice(ctx)) +} + +func TestMiddleware_impersonate_invalid_personate_header(t *testing.T) { + now := test.Time("2018-06-30T18:30:00Z") + timeDispose := fakeTime(now) + defer timeDispose() + + db := test.InMemoryDB(t) + defer db.Close() + builder := db.User(1) + user := builder.User + user.Admin = true + db.Save(user) + builder.NewDevice(2, "abc", "test") + spy := &requestSpy{} + recorder := httptest.NewRecorder() + + request := httptest.NewRequest("GET", "/test?token=abc", nil) + request.Header.Set("X-Traggo-Impersonate", "invalid") + Middleware(db.DB)(spy).ServeHTTP(recorder, request) + + response := recorder.Result() + assert.Equal(t, 200, response.StatusCode) + + ctx := spy.req.Context() + assert.Nil(t, GetUser(ctx)) + assert.Nil(t, GetDevice(ctx)) +} + +func TestMiddleware_impersonate_non_existing_user(t *testing.T) { + now := test.Time("2018-06-30T18:30:00Z") + timeDispose := fakeTime(now) + defer timeDispose() + + db := test.InMemoryDB(t) + defer db.Close() + builder := db.User(1) + user := builder.User + user.Admin = true + db.Save(user) + builder.NewDevice(2, "abc", "test") + spy := &requestSpy{} + recorder := httptest.NewRecorder() + + request := httptest.NewRequest("GET", "/test?token=abc", nil) + request.Header.Set("X-Traggo-Impersonate", "42") + Middleware(db.DB)(spy).ServeHTTP(recorder, request) + + response := recorder.Result() + assert.Equal(t, 200, response.StatusCode) + + ctx := spy.req.Context() + assert.Nil(t, GetUser(ctx)) + assert.Nil(t, GetDevice(ctx)) +} + +func TestMiddleware_impersonate_happy(t *testing.T) { + now := test.Time("2018-06-30T18:30:00Z") + timeDispose := fakeTime(now) + defer timeDispose() + + db := test.InMemoryDB(t) + defer db.Close() + admin_builder := db.User(1) + admin_user := admin_builder.User + admin_user.Admin = true + db.Save(admin_user) + admin_device := admin_builder.NewDevice(2, "abc", "test") + + builder := db.User(2) + user := builder.User + + spy := &requestSpy{} + recorder := httptest.NewRecorder() + + request := httptest.NewRequest("GET", "/test?token=abc", nil) + request.Header.Set("X-Traggo-Impersonate", "2") + Middleware(db.DB)(spy).ServeHTTP(recorder, request) + + response := recorder.Result() + assert.Equal(t, 200, response.StatusCode) + + ctx := spy.req.Context() + assert.Equal(t, &user, GetUser(ctx)) + + admin_device.ActiveAt = now + assert.Equal(t, &admin_device, GetDevice(ctx)) +} + +func TestGetCreateSession_panicsWhenMiddlewareWasNotExecuted(t *testing.T) { + assert.Panics(t, func() { + GetCreateSession(context.Background()) + }) +} + +func TestGetDestroySession_panicsWhenMiddlewareWasNotExecuted(t *testing.T) { + assert.Panics(t, func() { + GetDestroySession(context.Background()) + }) +} + +type requestSpy struct { + req *http.Request +} + +func (s *requestSpy) ServeHTTP(_ http.ResponseWriter, req *http.Request) { + s.req = req +} diff --git a/auth/rand/random.go b/auth/rand/random.go new file mode 100644 index 0000000..5ef4f84 --- /dev/null +++ b/auth/rand/random.go @@ -0,0 +1,43 @@ +package rand + +import ( + "crypto/rand" + + "github.com/rs/zerolog/log" +) + +var ( + chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + + randRead = rand.Read +) + +func init() { + // exits with 1 when crypto/rand is not available + Token(1) +} + +func randBytes(count int) ([]byte, error) { + bytes := make([]byte, count) + _, err := randRead(bytes) + return bytes, err +} + +// Token returns a random token. +func Token(count int) string { + bytes, err := randBytes(count) + if err != nil { + log.Panic().Msg("crypto/rand is not available") + } + return bytesToString(bytes, count) +} + +func bytesToString(bytes []byte, count int) string { + token := make([]rune, count) + + for index, item := range bytes { + token[index] = chars[int(item)%len(chars)] + } + + return string(token) +} diff --git a/auth/rand/random_test.go b/auth/rand/random_test.go new file mode 100644 index 0000000..cd260e9 --- /dev/null +++ b/auth/rand/random_test.go @@ -0,0 +1,30 @@ +package rand + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToken(t *testing.T) { + for i := 1; i < 10; i++ { + assert.Len(t, Token(i), i) + } + assert.Len(t, Token(120), 120) + assert.Len(t, Token(222), 222) + assert.Len(t, Token(555665), 555665) +} + +func TestToken_panics(t *testing.T) { + old := randRead + defer func() { + randRead = old + }() + randRead = func(b []byte) (n int, err error) { + return 0, errors.New("oops") + } + assert.Panics(t, func() { + Token(1) + }) +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..9e3452e --- /dev/null +++ b/config/config.go @@ -0,0 +1,104 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/joho/godotenv" + "github.com/kelseyhightower/envconfig" + "github.com/rs/zerolog" + "github.com/traggo/server/config/mode" +) + +var ( + prefix = "traggo" + files = []string{".env.development.local", ".env.development", ".env.local", ".env"} + absoluteFiles = []string{"/etc/traggo/server.ini"} + osExecutable = os.Executable + osStat = os.Stat +) + +// Config represents the application configuration. +type Config struct { + Port int `default:"3030"` + LogLevel LogLevel `default:"info" split_words:"true"` + DefaultUserName string `default:"admin" split_words:"true"` + DefaultUserPass string `default:"admin" split_words:"true"` + PassStrength int `default:"10" split_words:"true"` + DatabaseDialect string `default:"sqlite3" split_words:"true"` + DatabaseConnection string `default:"data/traggo.db" split_words:"true"` +} + +// Get loads the application config. +func Get() (Config, []FutureLog) { + var logs []FutureLog + dir, log := getExecutableOrWorkDir() + if log != nil { + logs = append(logs, *log) + } + + for _, file := range getFiles(dir) { + _, fileErr := osStat(file) + if fileErr == nil { + if err := godotenv.Load(file); err != nil { + logs = append(logs, FutureLog{ + Level: zerolog.FatalLevel, + Msg: fmt.Sprintf("cannot load file %s: %s", file, err)}) + } else { + logs = append(logs, FutureLog{ + Level: zerolog.DebugLevel, + Msg: fmt.Sprintf("Loading file %s", file)}) + } + } else if os.IsNotExist(fileErr) { + continue + } else { + logs = append(logs, FutureLog{ + Level: zerolog.WarnLevel, + Msg: fmt.Sprintf("cannot read file %s because %s", file, fileErr)}) + } + } + + config := Config{} + err := envconfig.Process(prefix, &config) + if err != nil { + logs = append(logs, FutureLog{ + Level: zerolog.FatalLevel, + Msg: fmt.Sprintf("cannot parse env params: %s", err)}) + } + + return config, logs +} + +func getExecutableOrWorkDir() (string, *FutureLog) { + dir, err := getExecutableDir() + // when using `go run main.go` the executable lives in th temp directory therefore the env.development + // will not be read, this enforces that the current work directory is used in dev mode. + if err != nil || mode.Get() == mode.Dev { + return filepath.Dir("."), err + } + return dir, nil +} + +func getExecutableDir() (string, *FutureLog) { + ex, err := osExecutable() + if err != nil { + return "", &FutureLog{ + Level: zerolog.ErrorLevel, + Msg: "Could not get path of executable using working directory instead. " + err.Error()} + } + return filepath.Dir(ex), nil +} + +func getFiles(relativeTo string) []string { + var result []string + for _, file := range files { + result = append(result, filepath.Join(relativeTo, file)) + } + homeDir, err := os.UserHomeDir() + if err == nil { + result = append(result, filepath.Join(homeDir, ".config/traggo/server.ini")) + } + result = append(result, absoluteFiles...) + return result +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..8c3f136 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,160 @@ +package config + +import ( + "errors" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/traggo/server/config/mode" +) + +func TestMain(m *testing.M) { + absoluteFiles = []string{} + os.Exit(m.Run()) +} + +func TestGet_withoutFiles_containsDefault(t *testing.T) { + conf, logs := Get() + + assert.Empty(t, logs) + assert.Equal(t, 3030, conf.Port) +} + +func TestGet_withoutFiles_usesEnvVariables(t *testing.T) { + assert.Nil(t, os.Setenv("TRAGGO_PORT", "9999")) + defer os.Unsetenv("TRAGGO_PORT") + + conf, logs := Get() + + assert.Empty(t, logs) + assert.Equal(t, 9999, conf.Port) +} + +func TestGet_withFile_usesFile(t *testing.T) { + defer os.Unsetenv("TRAGGO_PORT") + assert.Nil(t, ioutil.WriteFile(".env", []byte("TRAGGO_PORT=5555"), 0777)) + defer os.Remove(".env") + + conf, logs := Get() + + expected := []FutureLog{{ + Msg: `Loading file .env`, + Level: zerolog.DebugLevel, + }} + assert.Equal(t, expected, logs) + assert.Equal(t, 5555, conf.Port) +} + +func TestGet_withInvalidEnvParam_errs(t *testing.T) { + assert.Nil(t, os.Setenv("TRAGGO_PORT", "asdasd")) + defer os.Unsetenv("TRAGGO_PORT") + + _, logs := Get() + + expected := []FutureLog{{ + Msg: `cannot parse env params: envconfig.Process: assigning TRAGGO_PORT to Port: converting 'asdasd' to type int. details: strconv.ParseInt: parsing "asdasd": invalid syntax`, + Level: zerolog.FatalLevel, + }} + assert.Equal(t, expected, logs) +} + +func TestGet_prodMode_findFileRelativeToExecutable(t *testing.T) { + defer os.Unsetenv("TRAGGO_PORT") + oldMode := mode.Get() + mode.Set(mode.Prod) + defer func() { + mode.Set(oldMode) + }() + old := osExecutable + defer func() { + osExecutable = old + }() + osExecutable = func() (string, error) { + return "./testpath/some.exe", nil + } + assert.Nil(t, os.MkdirAll("./testpath", 0777)) + defer os.RemoveAll("./testpath") + + assert.Nil(t, ioutil.WriteFile("./testpath/.env", []byte("TRAGGO_PORT=6666"), 0777)) + + conf, logs := Get() + + expected := []FutureLog{{ + Msg: `Loading file ` + filepath.Join("testpath", ".env"), + Level: zerolog.DebugLevel, + }} + assert.Equal(t, expected, logs) + assert.Equal(t, 6666, conf.Port) +} + +func TestGet_errsOnOsExecutable(t *testing.T) { + defer os.Unsetenv("TRAGGO_PORT") + oldMode := mode.Get() + mode.Set(mode.Prod) + defer func() { + mode.Set(oldMode) + }() + old := osExecutable + defer func() { + osExecutable = old + }() + osExecutable = func() (string, error) { + return "", errors.New("oops") + } + + _, logs := Get() + + expected := []FutureLog{{ + Msg: `Could not get path of executable using working directory instead. oops`, + Level: zerolog.ErrorLevel, + }} + assert.Equal(t, expected, logs) +} + +func TestGet_withoutFilePermission(t *testing.T) { + defer os.Unsetenv("TRAGGO_PORT") + old := osStat + defer func() { + osStat = old + }() + osStat = func(name string) (os.FileInfo, error) { + if name == ".env" { + return nil, os.ErrPermission + } + return os.Stat(name) + } + + _, logs := Get() + + expected := []FutureLog{{ + Msg: `cannot read file .env because permission denied`, + Level: zerolog.WarnLevel, + }} + assert.Equal(t, expected, logs) +} + +func TestGet_checksAbsoluteFiles(t *testing.T) { + defer os.Unsetenv("TRAGGO_PORT") + assert.Nil(t, os.MkdirAll("./absolutepath", 0777)) + defer os.RemoveAll("./absolutepath") + path, err := filepath.Abs("./absolutepath/.env") + assert.Nil(t, err) + absoluteFiles = []string{path} + defer func() { + absoluteFiles = []string{} + }() + assert.Nil(t, ioutil.WriteFile("./absolutepath/.env", []byte("TRAGGO_PORT=7777"), 0777)) + + conf, logs := Get() + + expected := []FutureLog{{ + Msg: `Loading file ` + path, + Level: zerolog.DebugLevel, + }} + assert.Equal(t, expected, logs) + assert.Equal(t, 7777, conf.Port) +} diff --git a/config/error.go b/config/error.go new file mode 100644 index 0000000..e683a9b --- /dev/null +++ b/config/error.go @@ -0,0 +1,10 @@ +package config + +import "github.com/rs/zerolog" + +// FutureLog is an intermediate type for log messages. It is used before the config was loaded because without loaded +// config we do not know the log level, so we log these messages once the config was initialized. +type FutureLog struct { + Level zerolog.Level + Msg string +} diff --git a/config/loglevel.go b/config/loglevel.go new file mode 100644 index 0000000..786946d --- /dev/null +++ b/config/loglevel.go @@ -0,0 +1,25 @@ +package config + +import ( + "errors" + + "github.com/rs/zerolog" +) + +// LogLevel type that provides helper methods for decoding. +type LogLevel zerolog.Level + +// Decode decodes a string to a log level. +func (ll *LogLevel) Decode(value string) error { + if level, err := zerolog.ParseLevel(value); err == nil { + *ll = LogLevel(level) + return nil + } + *ll = LogLevel(zerolog.InfoLevel) + return errors.New("unknown log level") +} + +// AsZeroLogLevel converts the LogLevel to a zerolog.Level. +func (ll LogLevel) AsZeroLogLevel() zerolog.Level { + return zerolog.Level(ll) +} diff --git a/config/loglevel_test.go b/config/loglevel_test.go new file mode 100644 index 0000000..6bee82a --- /dev/null +++ b/config/loglevel_test.go @@ -0,0 +1,22 @@ +package config + +import ( + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func TestLogLevel_Decode_success(t *testing.T) { + ll := new(LogLevel) + err := ll.Decode("fatal") + assert.Nil(t, err) + assert.Equal(t, ll.AsZeroLogLevel(), zerolog.FatalLevel) +} + +func TestLogLevel_Decode_fail(t *testing.T) { + ll := new(LogLevel) + err := ll.Decode("asdasdasdasdasdasd") + assert.EqualError(t, err, "unknown log level") + assert.Equal(t, ll.AsZeroLogLevel(), zerolog.InfoLevel) +} diff --git a/config/mode/mode.go b/config/mode/mode.go new file mode 100644 index 0000000..6a9204d --- /dev/null +++ b/config/mode/mode.go @@ -0,0 +1,20 @@ +package mode + +const ( + // Dev for development mode + Dev = "dev" + // Prod for production mode + Prod = "prod" +) + +var mode = Dev + +// Set sets the new mode. +func Set(newMode string) { + mode = newMode +} + +// Get returns the current mode. +func Get() string { + return mode +} diff --git a/config/mode/mode_test.go b/config/mode/mode_test.go new file mode 100644 index 0000000..f2ce63a --- /dev/null +++ b/config/mode/mode_test.go @@ -0,0 +1,17 @@ +package mode + +import ( + "testing" + + "github.com/magiconair/properties/assert" +) + +func TestGet(t *testing.T) { + mode = Prod + assert.Equal(t, Prod, Get()) +} + +func TestSet(t *testing.T) { + Set(Prod) + assert.Equal(t, Prod, mode) +} diff --git a/dashboard/convert/dashboard.go b/dashboard/convert/dashboard.go new file mode 100644 index 0000000..f3e1cf8 --- /dev/null +++ b/dashboard/convert/dashboard.go @@ -0,0 +1,37 @@ +package convert + +import ( + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// ToExternalDashboards converts dashboards. +func ToExternalDashboards(dashboards []model.Dashboard) ([]*gqlmodel.Dashboard, error) { + result := []*gqlmodel.Dashboard{} + for _, dashboard := range dashboards { + if converted, err := ToExternalDashboard(dashboard); err == nil { + result = append(result, converted) + } else { + return nil, err + } + } + return result, nil +} + +// ToExternalDashboard converts a dashboard. +func ToExternalDashboard(dashboard model.Dashboard) (*gqlmodel.Dashboard, error) { + entries, err := toExternalEntries(dashboard.Entries) + if err != nil { + return nil, err + } + ranges, err := toExternalDashboardsRanges(dashboard.Ranges) + if err != nil { + return nil, err + } + return &gqlmodel.Dashboard{ + ID: dashboard.ID, + Name: dashboard.Name, + Items: entries, + Ranges: ranges, + }, nil +} diff --git a/dashboard/convert/dashboardtag.go b/dashboard/convert/dashboardtag.go new file mode 100644 index 0000000..884be1b --- /dev/null +++ b/dashboard/convert/dashboardtag.go @@ -0,0 +1,39 @@ +package convert + +import ( + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +func tagFiltersToExternal(tags []model.DashboardTagFilter, include bool) []*gqlmodel.TimeSpanTag { + if len(tags) == 0 { + return nil + } + + result := []*gqlmodel.TimeSpanTag{} + for _, tag := range tags { + if tag.Include == include { + result = append(result, &gqlmodel.TimeSpanTag{ + Key: tag.Key, + Value: tag.StringValue, + }) + } + } + return result +} + +func TagFiltersToInternal(gqls []*gqlmodel.InputTimeSpanTag, include bool) []model.DashboardTagFilter { + result := make([]model.DashboardTagFilter, 0) + for _, tag := range gqls { + result = append(result, tagFilterToInternal(*tag, include)) + } + return result +} + +func tagFilterToInternal(gqls gqlmodel.InputTimeSpanTag, include bool) model.DashboardTagFilter { + return model.DashboardTagFilter{ + Key: gqls.Key, + StringValue: gqls.Value, + Include: include, + } +} diff --git a/dashboard/convert/entry.go b/dashboard/convert/entry.go new file mode 100644 index 0000000..a61f6f8 --- /dev/null +++ b/dashboard/convert/entry.go @@ -0,0 +1,59 @@ +package convert + +import ( + "strings" + + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +func toExternalEntries(entries []model.DashboardEntry) ([]*gqlmodel.DashboardEntry, error) { + result := []*gqlmodel.DashboardEntry{} + for _, entry := range entries { + converted, err := ToExternalEntry(entry) + if err != nil { + return nil, err + } + result = append(result, converted) + } + return result, nil +} + +// ToExternalEntry converts entries. +func ToExternalEntry(entry model.DashboardEntry) (*gqlmodel.DashboardEntry, error) { + mobilePosition, err := toExternalPosition(entry.MobilePosition) + if err != nil { + return &gqlmodel.DashboardEntry{}, err + } + desktopPos, err := toExternalPosition(entry.DesktopPosition) + if err != nil { + return &gqlmodel.DashboardEntry{}, err + } + pos := gqlmodel.ResponsiveDashboardEntryPos{ + Mobile: enhancePos(mobilePosition, "mobile"), + Desktop: enhancePos(desktopPos, "desktop"), + } + dateRange := &gqlmodel.RelativeOrStaticRange{ + From: entry.RangeFrom, + To: entry.RangeTo, + } + stats := &gqlmodel.StatsSelection{ + Interval: ExternalInterval(entry.Interval), + Tags: strings.Split(entry.Keys, ","), + Range: dateRange, + ExcludeTags: tagFiltersToExternal(entry.TagFilters, false), + IncludeTags: tagFiltersToExternal(entry.TagFilters, true), + } + if entry.RangeID != model.NoRangeIDDefined { + stats.RangeID = &entry.RangeID + stats.Range = nil + } + return &gqlmodel.DashboardEntry{ + ID: entry.ID, + Title: entry.Title, + Total: entry.Total, + Pos: &pos, + StatsSelection: stats, + EntryType: ExternalEntryType(entry.Type), + }, nil +} diff --git a/dashboard/convert/entrytype.go b/dashboard/convert/entrytype.go new file mode 100644 index 0000000..e129f8c --- /dev/null +++ b/dashboard/convert/entrytype.go @@ -0,0 +1,46 @@ +package convert + +import ( + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// InternalEntryType converts entry type. +func InternalEntryType(entryType gqlmodel.EntryType) model.DashboardType { + switch entryType { + case gqlmodel.EntryTypeBarChart: + return model.TypeBarChart + case gqlmodel.EntryTypePieChart: + return model.TypePieChart + case gqlmodel.EntryTypeStackedBarChart: + return model.TypeStackedBarChart + case gqlmodel.EntryTypeLineChart: + return model.TypeLineChart + case gqlmodel.EntryTypeHorizontalTable: + return model.HorizontalTable + case gqlmodel.EntryTypeVerticalTable: + return model.VerticalTable + default: + panic("unknown entry type " + entryType) + } +} + +// ExternalEntryType converts entry type. +func ExternalEntryType(entryType model.DashboardType) gqlmodel.EntryType { + switch entryType { + case model.TypeBarChart: + return gqlmodel.EntryTypeBarChart + case model.TypePieChart: + return gqlmodel.EntryTypePieChart + case model.TypeStackedBarChart: + return gqlmodel.EntryTypeStackedBarChart + case model.TypeLineChart: + return gqlmodel.EntryTypeLineChart + case model.HorizontalTable: + return gqlmodel.EntryTypeHorizontalTable + case model.VerticalTable: + return gqlmodel.EntryTypeVerticalTable + default: + panic("unknown entry type " + entryType) + } +} diff --git a/dashboard/convert/interval.go b/dashboard/convert/interval.go new file mode 100644 index 0000000..1ae9c93 --- /dev/null +++ b/dashboard/convert/interval.go @@ -0,0 +1,46 @@ +package convert + +import ( + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// InternalInterval converts interval. +func InternalInterval(interval gqlmodel.StatsInterval) model.Interval { + switch interval { + case gqlmodel.StatsIntervalHourly: + return model.IntervalHourly + case gqlmodel.StatsIntervalDaily: + return model.IntervalDaily + case gqlmodel.StatsIntervalWeekly: + return model.IntervalWeekly + case gqlmodel.StatsIntervalMonthly: + return model.IntervalMonthly + case gqlmodel.StatsIntervalYearly: + return model.IntervalYearly + case gqlmodel.StatsIntervalSingle: + return model.IntervalSingle + default: + panic("unknown interval type " + interval) + } +} + +// ExternalInterval converts interval. +func ExternalInterval(interval model.Interval) gqlmodel.StatsInterval { + switch interval { + case model.IntervalHourly: + return gqlmodel.StatsIntervalHourly + case model.IntervalDaily: + return gqlmodel.StatsIntervalDaily + case model.IntervalWeekly: + return gqlmodel.StatsIntervalWeekly + case model.IntervalMonthly: + return gqlmodel.StatsIntervalMonthly + case model.IntervalYearly: + return gqlmodel.StatsIntervalYearly + case model.IntervalSingle: + return gqlmodel.StatsIntervalSingle + default: + panic("unknown interval type " + interval) + } +} diff --git a/dashboard/convert/position.go b/dashboard/convert/position.go new file mode 100644 index 0000000..b8a59db --- /dev/null +++ b/dashboard/convert/position.go @@ -0,0 +1,111 @@ +package convert + +import ( + "encoding/json" + + "github.com/jinzhu/copier" + "github.com/rs/zerolog/log" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +func enhancePos(pos *gqlmodel.DashboardEntryPos, size string) *gqlmodel.DashboardEntryPos { + switch size { + case "mobile": + pos.MinH = 2 + pos.MinW = 1 + case "desktop": + pos.MinH = 3 + pos.MinW = 2 + default: + panic("unknown size") + } + if pos.MinW > pos.W { + pos.W = pos.MinW + } + if pos.MinH > pos.H { + pos.H = pos.MinH + } + return pos +} + +// EmptyPos returns a empty position. +func EmptyPos() string { + internal := internalDashboardPositionV1{ + Version: "v1", + H: 0, + W: 0, + X: 0, + Y: 0, + } + + if bytes, err := json.Marshal(&internal); err != nil { + panic(err) + } else { + return string(bytes) + } +} + +func toInternalPosition(external gqlmodel.InputDashboardEntryPos) (string, error) { + internal := internalDashboardPositionV1{Version: "v1"} + if err := copier.Copy(&internal, &external); err != nil { + return "", err + } + bytes, err := json.Marshal(&internal) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ApplyPos copies the position to the external position. +func ApplyPos(entry *model.DashboardEntry, pos *gqlmodel.InputResponsiveDashboardEntryPos) error { + if pos == nil { + return nil + } + + if pos.Mobile != nil { + lpos, err := toInternalPosition(*pos.Mobile) + if err != nil { + return err + } + entry.MobilePosition = lpos + } + if pos.Desktop != nil { + mpos, err := toInternalPosition(*pos.Desktop) + if err != nil { + return err + } + entry.DesktopPosition = mpos + } + return nil +} + +func toExternalPosition(pos string) (*gqlmodel.DashboardEntryPos, error) { + if pos == "" { + return nil, nil + } + internal := internalDashboardPositionV1{} + if err := json.Unmarshal([]byte(pos), &internal); err != nil { + return nil, err + } + + if internal.Version != "v1" { + log.Error().Interface("pos", internal).Msg("Invalid position version") + } + external := gqlmodel.DashboardEntryPos{} + + if err := copier.Copy(&external, &internal); err != nil { + return nil, err + } + + return &external, nil +} + +type internalDashboardPositionV1 struct { + Version string + W int `json:"w"` + H int `json:"h"` + X int `json:"x"` + Y int `json:"y"` +} diff --git a/dashboard/convert/range.go b/dashboard/convert/range.go new file mode 100644 index 0000000..a69368c --- /dev/null +++ b/dashboard/convert/range.go @@ -0,0 +1,46 @@ +package convert + +import ( + "fmt" + + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/time" +) + +func toExternalDashboardsRanges(ranges []model.DashboardRange) ([]*gqlmodel.NamedDateRange, error) { + result := []*gqlmodel.NamedDateRange{} + for _, xrange := range ranges { + result = append(result, ToExternalDashboardRange(xrange)) + } + return result, nil +} + +// ToExternalDashboardRange converts a range. +func ToExternalDashboardRange(xrange model.DashboardRange) *gqlmodel.NamedDateRange { + return &gqlmodel.NamedDateRange{ + ID: xrange.ID, + Name: xrange.Name, + Editable: xrange.Editable, + Range: &gqlmodel.RelativeOrStaticRange{ + From: xrange.From, + To: xrange.To, + }, + } +} + +// ToInternalDashboardRange converts a range. +func ToInternalDashboardRange(xrange gqlmodel.InputNamedDateRange) (model.DashboardRange, error) { + if err := time.Validate(xrange.Range.From); err != nil { + return model.DashboardRange{}, fmt.Errorf("range from (%s) invalid: %s", xrange.Range.From, err) + } + if err := time.Validate(xrange.Range.To); err != nil { + return model.DashboardRange{}, fmt.Errorf("range to (%s) invalid: %s", xrange.Range.To, err) + } + return model.DashboardRange{ + Editable: xrange.Editable, + From: xrange.Range.From, + To: xrange.Range.To, + Name: xrange.Name, + }, nil +} diff --git a/dashboard/create.go b/dashboard/create.go new file mode 100644 index 0000000..9f3b670 --- /dev/null +++ b/dashboard/create.go @@ -0,0 +1,27 @@ +package dashboard + +import ( + "context" + + "github.com/traggo/server/dashboard/convert" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// CreateDashboard creates a dashboard. +func (r *ResolverForDashboard) CreateDashboard(ctx context.Context, name string) (*gqlmodel.Dashboard, error) { + userID := auth.GetUser(ctx).ID + dashboard := model.Dashboard{ + UserID: userID, + Name: name, + } + + create := r.DB.Create(&dashboard) + if create.Error != nil { + return &gqlmodel.Dashboard{}, create.Error + } + + return convert.ToExternalDashboard(dashboard) +} diff --git a/dashboard/dashboard_test.go b/dashboard/dashboard_test.go new file mode 100644 index 0000000..425d80f --- /dev/null +++ b/dashboard/dashboard_test.go @@ -0,0 +1,106 @@ +package dashboard + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestDashboard(t *testing.T) { + + db := test.InMemoryDB(t) + defer db.Close() + db.User(1) + db.User(2) + + resolver := NewResolverForDashboard(db.DB) + + // initial no dashboards + user1 := fake.User(1) + user2 := fake.User(2) + dashboards, err := resolver.Dashboards(user1) + require.NoError(t, err) + require.Empty(t, dashboards) + + dashboard, err := resolver.CreateDashboard(user1, "cool dashboard") + require.NoError(t, err) + expectAdded := &gqlmodel.Dashboard{ + ID: 1, + Name: "cool dashboard", + Ranges: []*gqlmodel.NamedDateRange{}, + Items: []*gqlmodel.DashboardEntry{}, + } + require.Equal(t, expectAdded, dashboard) + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Equal(t, []*gqlmodel.Dashboard{ + { + ID: 1, + Name: "cool dashboard", + Ranges: []*gqlmodel.NamedDateRange{}, + Items: []*gqlmodel.DashboardEntry{}, + }, + }, dashboards) + dashboards, err = resolver.Dashboards(user2) + require.NoError(t, err) + require.Empty(t, dashboards) + + dashboard, err = resolver.UpdateDashboard(user2, 1, "kek") + require.EqualError(t, err, "dashboard does not exist") + require.Nil(t, dashboard) + + dashboard, err = resolver.UpdateDashboard(user2, 55, "kek") + require.EqualError(t, err, "dashboard does not exist") + require.Nil(t, dashboard) + + dashboard, err = resolver.UpdateDashboard(user1, 1, "kek") + require.NoError(t, err) + + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Equal(t, []*gqlmodel.Dashboard{ + { + ID: 1, + Name: "kek", + Ranges: []*gqlmodel.NamedDateRange{}, + Items: []*gqlmodel.DashboardEntry{}, + }, + }, dashboards) + + dashboard, err = resolver.RemoveDashboard(user2, 1) + require.EqualError(t, err, "dashboard does not exist") + require.Nil(t, dashboard) + + dashboard, err = resolver.RemoveDashboard(user2, 55) + require.EqualError(t, err, "dashboard does not exist") + require.Nil(t, dashboard) + + dashboard, err = resolver.RemoveDashboard(user1, 1) + require.NoError(t, err) + + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Empty(t, dashboards) +} + +func TestDeleteDashboard(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + dbOne := db.User(1).Dashboard("test") + dbOne.Range("r") + dbOne.Entry("e") + dbTwo := db.User(2).Dashboard("test") + dbTwo.Range("r") + dbTwo.Entry("e") + + resolver := NewResolverForDashboard(db.DB) + _, err := resolver.RemoveDashboard(fake.User(dbOne.User.ID), dbOne.Dashboard.ID) + require.NoError(t, err) + + dbOne.AssertExists(false).AssertHasEntry("e", false).AssertHasRange("r", false) + dbTwo.AssertExists(true).AssertHasEntry("e", true).AssertHasRange("r", true) +} diff --git a/dashboard/dashboardresolver.go b/dashboard/dashboardresolver.go new file mode 100644 index 0000000..fb76907 --- /dev/null +++ b/dashboard/dashboardresolver.go @@ -0,0 +1,27 @@ +package dashboard + +import ( + "github.com/jinzhu/gorm" + "github.com/traggo/server/dashboard/dbrange" + "github.com/traggo/server/dashboard/entry" +) + +// ResolverForDashboard resolves dashboard specific things. +type ResolverForDashboard struct { + DB *gorm.DB + entry.ResolverForEntry + dbrange.ResolverForRange +} + +// NewResolverForDashboard creates a new resolver. +func NewResolverForDashboard(db *gorm.DB) ResolverForDashboard { + return ResolverForDashboard{ + DB: db, + ResolverForEntry: entry.ResolverForEntry{ + DB: db, + }, + ResolverForRange: dbrange.ResolverForRange{ + DB: db, + }, + } +} diff --git a/dashboard/dbrange/add.go b/dashboard/dbrange/add.go new file mode 100644 index 0000000..c7c6b8f --- /dev/null +++ b/dashboard/dbrange/add.go @@ -0,0 +1,28 @@ +package dbrange + +import ( + "context" + + "github.com/traggo/server/auth" + "github.com/traggo/server/dashboard/convert" + "github.com/traggo/server/dashboard/util" + "github.com/traggo/server/generated/gqlmodel" +) + +// AddDashboardRange adds a dashboard range. +func (r *ResolverForRange) AddDashboardRange(ctx context.Context, dashboardID int, rangeArg gqlmodel.InputNamedDateRange) (*gqlmodel.NamedDateRange, error) { + userID := auth.GetUser(ctx).ID + if _, err := util.FindDashboard(r.DB, userID, dashboardID); err != nil { + return nil, err + } + + rangeToAdd, err := convert.ToInternalDashboardRange(rangeArg) + if err != nil { + return nil, err + } + + rangeToAdd.DashboardID = dashboardID + + save := r.DB.Create(&rangeToAdd) + return convert.ToExternalDashboardRange(rangeToAdd), save.Error +} diff --git a/dashboard/dbrange/dbrangeresolver.go b/dashboard/dbrange/dbrangeresolver.go new file mode 100644 index 0000000..9cc9ff9 --- /dev/null +++ b/dashboard/dbrange/dbrangeresolver.go @@ -0,0 +1,8 @@ +package dbrange + +import "github.com/jinzhu/gorm" + +// ResolverForRange resolves range specific things. +type ResolverForRange struct { + DB *gorm.DB +} diff --git a/dashboard/dbrange/ranges_test.go b/dashboard/dbrange/ranges_test.go new file mode 100644 index 0000000..2fd1aa0 --- /dev/null +++ b/dashboard/dbrange/ranges_test.go @@ -0,0 +1,201 @@ +package dbrange_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/dashboard" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestRanges(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + resolver := dashboard.NewResolverForDashboard(db.DB) + + // initial no dashboards + user1 := fake.User(1) + user2 := fake.User(2) + db.User(1) + db.User(2) + dashboards, err := resolver.Dashboards(user1) + require.NoError(t, err) + require.Empty(t, dashboards) + + dashboard, err := resolver.CreateDashboard(user1, "cool dashboard") + require.NoError(t, err) + expectAdded := &gqlmodel.Dashboard{ + ID: 1, + Name: "cool dashboard", + Ranges: []*gqlmodel.NamedDateRange{}, + Items: []*gqlmodel.DashboardEntry{}, + } + require.Equal(t, expectAdded, dashboard) + + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Equal(t, []*gqlmodel.Dashboard{expectAdded}, dashboards) + + dashboards, err = resolver.Dashboards(user2) + require.NoError(t, err) + require.Empty(t, dashboards) + + xrange, err := resolver.AddDashboardRange(user1, dashboard.ID, gqlmodel.InputNamedDateRange{ + Name: "new range", + Editable: false, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-1d", + To: "now-2d", + }, + }) + require.NoError(t, err) + require.Equal(t, &gqlmodel.NamedDateRange{ + ID: 1, + Name: "new range", + Editable: false, + Range: &gqlmodel.RelativeOrStaticRange{ + From: "now-1d", + To: "now-2d", + }, + }, xrange) + _, err = resolver.AddDashboardRange(user1, dashboard.ID, gqlmodel.InputNamedDateRange{ + Name: "new range", + Editable: false, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-1", + To: "now-2d", + }, + }) + require.EqualError(t, err, "range from (now-1) invalid: expected unit at the end but got nothing") + _, err = resolver.AddDashboardRange(user1, dashboard.ID, gqlmodel.InputNamedDateRange{ + Name: "new range", + Editable: false, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-1d", + To: "now-2", + }, + }) + require.EqualError(t, err, "range to (now-2) invalid: expected unit at the end but got nothing") + _, err = resolver.AddDashboardRange(user1, 100, gqlmodel.InputNamedDateRange{ + Name: "new range", + Editable: false, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-1d", + To: "now-2", + }, + }) + require.EqualError(t, err, "dashboard does not exist") + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Equal(t, []*gqlmodel.Dashboard{{ + ID: 1, + Name: "cool dashboard", + Ranges: []*gqlmodel.NamedDateRange{{ + ID: 1, + Name: "new range", + Editable: false, + Range: &gqlmodel.RelativeOrStaticRange{ + From: "now-1d", + To: "now-2d", + }, + }}, + Items: []*gqlmodel.DashboardEntry{}, + }}, dashboards) + + _, err = resolver.UpdateDashboardRange(user2, 1, gqlmodel.InputNamedDateRange{ + Name: "my range", + Editable: true, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-1d", + To: "now-2d/w", + }, + }) + require.EqualError(t, err, "dashboard does not exist") + xrange, err = resolver.UpdateDashboardRange(user1, 1, gqlmodel.InputNamedDateRange{ + Name: "my range", + Editable: true, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-1d", + To: "now-2d/w", + }, + }) + require.Equal(t, &gqlmodel.NamedDateRange{ + ID: 1, + Name: "my range", + Editable: true, + Range: &gqlmodel.RelativeOrStaticRange{ + From: "now-1d", + To: "now-2d/w", + }, + }, xrange) + _, err = resolver.UpdateDashboardRange(user1, 1, gqlmodel.InputNamedDateRange{ + Name: "my range", + Editable: true, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-1d", + To: "now-2", + }, + }) + require.EqualError(t, err, "range to (now-2) invalid: expected unit at the end but got nothing") + _, err = resolver.UpdateDashboardRange(user1, 44, gqlmodel.InputNamedDateRange{ + Name: "my range", + Editable: true, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-1d", + To: "now-2", + }, + }) + require.EqualError(t, err, "dashboard range does not exist") + + entry, err := resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "other", true, gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"abc"}, + RangeID: &xrange.ID, + }, &gqlmodel.InputResponsiveDashboardEntryPos{Desktop: &gqlmodel.InputDashboardEntryPos{ + H: 1, + W: 2, + X: 1, + Y: 3, + }}) + require.NoError(t, err) + _, err = resolver.RemoveDashboardRange(user1, xrange.ID) + require.EqualError(t, err, "range is used in entries: other") + _, err = resolver.RemoveDashboardEntry(user1, entry.ID) + require.NoError(t, err) + + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Equal(t, []*gqlmodel.Dashboard{{ + ID: 1, + Name: "cool dashboard", + Ranges: []*gqlmodel.NamedDateRange{{ + ID: 1, + Name: "my range", + Editable: true, + Range: &gqlmodel.RelativeOrStaticRange{ + From: "now-1d", + To: "now-2d/w", + }, + }}, + Items: []*gqlmodel.DashboardEntry{}, + }}, dashboards) + + _, err = resolver.RemoveDashboardRange(user2, 1) + require.EqualError(t, err, "dashboard does not exist") + _, err = resolver.RemoveDashboardRange(user2, 55) + require.EqualError(t, err, "dashboard range does not exist") + + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Len(t, dashboards[0].Ranges, 1) + + _, err = resolver.RemoveDashboardRange(user1, 1) + require.NoError(t, err) + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Len(t, dashboards[0].Ranges, 0) + +} diff --git a/dashboard/dbrange/remove.go b/dashboard/dbrange/remove.go new file mode 100644 index 0000000..c6af55a --- /dev/null +++ b/dashboard/dbrange/remove.go @@ -0,0 +1,43 @@ +package dbrange + +import ( + "context" + "errors" + "strings" + + "github.com/traggo/server/auth" + "github.com/traggo/server/dashboard/convert" + "github.com/traggo/server/dashboard/util" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// RemoveDashboardRange removes a dashboard range. +func (r *ResolverForRange) RemoveDashboardRange(ctx context.Context, rangeID int) (*gqlmodel.NamedDateRange, error) { + userID := auth.GetUser(ctx).ID + dashboardRange, err := util.FindDashboardRange(r.DB, rangeID) + if err != nil { + return nil, err + } + + if _, err := util.FindDashboard(r.DB, userID, dashboardRange.DashboardID); err != nil { + return nil, err + } + + entries := []model.DashboardEntry{} + if err := r.DB.Where(model.DashboardEntry{RangeID: rangeID}).Find(&entries).Error; err != nil { + return nil, err + } + + names := []string{} + for _, entry := range entries { + names = append(names, entry.Title) + } + + if len(entries) != 0 { + return nil, errors.New("range is used in entries: " + strings.Join(names, ",")) + } + + remove := r.DB.Delete(&model.DashboardRange{}, rangeID) + return convert.ToExternalDashboardRange(dashboardRange), remove.Error +} diff --git a/dashboard/dbrange/update.go b/dashboard/dbrange/update.go new file mode 100644 index 0000000..86f90b8 --- /dev/null +++ b/dashboard/dbrange/update.go @@ -0,0 +1,35 @@ +package dbrange + +import ( + "context" + + "github.com/traggo/server/auth" + "github.com/traggo/server/dashboard/convert" + "github.com/traggo/server/dashboard/util" + "github.com/traggo/server/generated/gqlmodel" +) + +// UpdateDashboardRange updates a dashboard range. +func (r *ResolverForRange) UpdateDashboardRange(ctx context.Context, rangeID int, rangeArg gqlmodel.InputNamedDateRange) (*gqlmodel.NamedDateRange, error) { + userID := auth.GetUser(ctx).ID + + dashboardRange, err := util.FindDashboardRange(r.DB, rangeID) + if err != nil { + return nil, err + } + + if _, err := util.FindDashboard(r.DB, userID, dashboardRange.DashboardID); err != nil { + return nil, err + } + + rangeToUpdate, err := convert.ToInternalDashboardRange(rangeArg) + if err != nil { + return nil, err + } + + rangeToUpdate.DashboardID = dashboardRange.DashboardID + rangeToUpdate.ID = dashboardRange.ID + + save := r.DB.Save(rangeToUpdate) + return convert.ToExternalDashboardRange(rangeToUpdate), save.Error +} diff --git a/dashboard/entry/add.go b/dashboard/entry/add.go new file mode 100644 index 0000000..c9f3fcf --- /dev/null +++ b/dashboard/entry/add.go @@ -0,0 +1,77 @@ +package entry + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/traggo/server/time" + + "github.com/traggo/server/dashboard/convert" + "github.com/traggo/server/dashboard/util" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// AddDashboardEntry adds a dashboard entry. +func (r *ResolverForEntry) AddDashboardEntry(ctx context.Context, dashboardID int, entryType gqlmodel.EntryType, title string, total bool, stats gqlmodel.InputStatsSelection, pos *gqlmodel.InputResponsiveDashboardEntryPos) (*gqlmodel.DashboardEntry, error) { + userID := auth.GetUser(ctx).ID + + if _, err := util.FindDashboard(r.DB, userID, dashboardID); err != nil { + return nil, err + } + + tagFilters := convert.TagFiltersToInternal(stats.ExcludeTags, false) + tagFilters = append(tagFilters, convert.TagFiltersToInternal(stats.IncludeTags, true)...) + + if err := tagsDuplicates(tagFilters); err != nil { + return nil, err + } + + entry := model.DashboardEntry{ + Keys: strings.Join(stats.Tags, ","), + Type: convert.InternalEntryType(entryType), + Title: title, + Total: total, + DashboardID: dashboardID, + Interval: convert.InternalInterval(stats.Interval), + MobilePosition: convert.EmptyPos(), + DesktopPosition: convert.EmptyPos(), + RangeID: -1, + TagFilters: tagFilters, + } + + if len(stats.Tags) == 0 { + return nil, errors.New("at least one tag is required") + } + + if err := convert.ApplyPos(&entry, pos); err != nil { + return &gqlmodel.DashboardEntry{}, err + } + + if stats.RangeID != nil { + if _, err := util.FindDashboardRange(r.DB, *stats.RangeID); err != nil { + return nil, err + } + entry.RangeID = *stats.RangeID + } else if stats.Range != nil { + entry.RangeID = model.NoRangeIDDefined + if err := time.Validate(stats.Range.From); err != nil { + return nil, fmt.Errorf("range from (%s) invalid: %s", stats.Range.From, err) + } + if err := time.Validate(stats.Range.To); err != nil { + return nil, fmt.Errorf("range to (%s) invalid: %s", stats.Range.To, err) + } + entry.RangeFrom = stats.Range.From + entry.RangeTo = stats.Range.To + } + + if err := r.DB.Save(&entry).Error; err != nil { + return nil, err + } + + return convert.ToExternalEntry(entry) +} diff --git a/dashboard/entry/dbentryresolver.go b/dashboard/entry/dbentryresolver.go new file mode 100644 index 0000000..ff1fd8f --- /dev/null +++ b/dashboard/entry/dbentryresolver.go @@ -0,0 +1,8 @@ +package entry + +import "github.com/jinzhu/gorm" + +// ResolverForEntry resolves dashboard entry things. +type ResolverForEntry struct { + DB *gorm.DB +} diff --git a/dashboard/entry/entries_test.go b/dashboard/entry/entries_test.go new file mode 100644 index 0000000..b1255a7 --- /dev/null +++ b/dashboard/entry/entries_test.go @@ -0,0 +1,526 @@ +package entry_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/dashboard" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestEntries(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + var bVal bool + + resolver := dashboard.NewResolverForDashboard(db.DB) + + // initial no dashboards + user1 := fake.User(1) + user2 := fake.User(2) + db.User(1) + db.User(2) + dashboards, err := resolver.Dashboards(user1) + require.NoError(t, err) + require.Empty(t, dashboards) + + dashboard, err := resolver.CreateDashboard(user1, "cool dashboard") + require.NoError(t, err) + expectAdded := &gqlmodel.Dashboard{ + ID: 1, + Name: "cool dashboard", + Ranges: []*gqlmodel.NamedDateRange{}, + Items: []*gqlmodel.DashboardEntry{}, + } + require.Equal(t, expectAdded, dashboard) + + _, err = resolver.AddDashboardEntry(user1, 5, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ + Interval: "", + Tags: []string{"hhol"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-1d", + To: "now-2d", + }, + }, nil) + require.EqualError(t, err, "dashboard does not exist") + _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalHourly, + Tags: []string{"hhol"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: p(55), + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-1d", + To: "now-2d", + }, + }, nil) + require.EqualError(t, err, "dashboard range does not exist") + _, err = resolver.AddDashboardEntry(user2, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ + Interval: "doubly", + Tags: []string{"hhol"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "", + To: "", + }, + }, nil) + require.EqualError(t, err, "dashboard does not exist") + _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"hhol"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-2d", + To: "now-2", + }, + }, nil) + require.EqualError(t, err, "range to (now-2) invalid: expected unit at the end but got nothing") + _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"hhol"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-2", + To: "now-2d", + }, + }, nil) + require.EqualError(t, err, "range from (now-2) invalid: expected unit at the end but got nothing") + _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-2d", + To: "now-2d", + }, + }, nil) + require.EqualError(t, err, "at least one tag is required") + + entry, err := resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "test", false, gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"abc"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-2d", + To: "now-5d", + }, + }, &gqlmodel.InputResponsiveDashboardEntryPos{Desktop: &gqlmodel.InputDashboardEntryPos{ + H: 1, + W: 2, + X: 1, + Y: 3, + }}) + require.NoError(t, err) + expectedEntry := &gqlmodel.DashboardEntry{ + ID: 1, + Title: "test", + Total: false, + StatsSelection: &gqlmodel.StatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"abc"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.RelativeOrStaticRange{ + From: "now-2d", + To: "now-5d", + }, + }, + Pos: &gqlmodel.ResponsiveDashboardEntryPos{ + Desktop: &gqlmodel.DashboardEntryPos{ + W: 2, + H: 3, + X: 1, + Y: 3, + MinW: 2, + MinH: 3, + }, + Mobile: &gqlmodel.DashboardEntryPos{ + W: 1, + H: 2, + X: 0, + Y: 0, + MinW: 1, + MinH: 2, + }, + }, + EntryType: gqlmodel.EntryTypeBarChart, + } + require.Equal(t, expectedEntry, entry) + + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Equal(t, dashboards[0].Items, []*gqlmodel.DashboardEntry{expectedEntry}) + xrange, err := resolver.AddDashboardRange(user1, 1, gqlmodel.InputNamedDateRange{ + Name: "range", + Editable: false, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-1d", + To: "now", + }, + }) + require.NoError(t, err) + _, err = resolver.AddDashboardEntry(user1, 1, gqlmodel.EntryTypeBarChart, "other", false, gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"abc"}, + RangeID: p(xrange.ID), + }, &gqlmodel.InputResponsiveDashboardEntryPos{Desktop: &gqlmodel.InputDashboardEntryPos{ + H: 1, + W: 2, + X: 1, + Y: 3, + }}) + require.NoError(t, err) + _, err = resolver.RemoveDashboardRange(user1, xrange.ID) + require.EqualError(t, err, "range is used in entries: other") + + chart := gqlmodel.EntryTypePieChart + _, err = resolver.UpdateDashboardEntry(user2, 1, &chart, nil, nil, nil, nil) + require.EqualError(t, err, "dashboard does not exist") + _, err = resolver.UpdateDashboardEntry(user1, 3, &chart, nil, nil, nil, nil) + require.EqualError(t, err, "entry does not exist") + _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &bVal, &gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"kek"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-2", + To: "now-5d", + }, + }, nil) + require.EqualError(t, err, "range from (now-2) invalid: expected unit at the end but got nothing") + _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &bVal, &gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"kek"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-2d", + To: "now-5", + }, + }, nil) + require.EqualError(t, err, "range to (now-5) invalid: expected unit at the end but got nothing") + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Equal(t, []*gqlmodel.DashboardEntry{ + { + ID: 1, + Title: "test", + Total: false, + StatsSelection: &gqlmodel.StatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"abc"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.RelativeOrStaticRange{ + From: "now-2d", + To: "now-5d", + }, + }, + Pos: &gqlmodel.ResponsiveDashboardEntryPos{ + Desktop: &gqlmodel.DashboardEntryPos{ + W: 2, + H: 3, + X: 1, + Y: 3, + MinW: 2, + MinH: 3, + }, + Mobile: &gqlmodel.DashboardEntryPos{ + W: 1, + H: 2, + X: 0, + Y: 0, + MinW: 1, + MinH: 2, + }, + }, + EntryType: gqlmodel.EntryTypeBarChart, + }, + { + ID: 2, + Title: "other", + Total: false, + StatsSelection: &gqlmodel.StatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"abc"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: p(1), + }, + Pos: &gqlmodel.ResponsiveDashboardEntryPos{ + Desktop: &gqlmodel.DashboardEntryPos{ + W: 2, + H: 3, + X: 1, + Y: 3, + MinW: 2, + MinH: 3, + }, + Mobile: &gqlmodel.DashboardEntryPos{ + W: 1, + H: 2, + X: 0, + Y: 0, + MinW: 1, + MinH: 2, + }, + }, + EntryType: gqlmodel.EntryTypeBarChart, + }, + }, dashboards[0].Items) + _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &bVal, &gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"kek"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.InputRelativeOrStaticRange{ + From: "now-2d", + To: "now-12d", + }, + }, &gqlmodel.InputResponsiveDashboardEntryPos{Desktop: &gqlmodel.InputDashboardEntryPos{ + H: 1, + W: 2, + X: 1, + Y: 3, + }}) + require.NoError(t, err) + + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Equal(t, []*gqlmodel.DashboardEntry{ + { + ID: 1, + Title: "cool title", + Total: false, + StatsSelection: &gqlmodel.StatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"kek"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: nil, + Range: &gqlmodel.RelativeOrStaticRange{ + From: "now-2d", + To: "now-12d", + }, + }, + Pos: &gqlmodel.ResponsiveDashboardEntryPos{ + Desktop: &gqlmodel.DashboardEntryPos{ + W: 2, + H: 3, + X: 1, + Y: 3, + MinW: 2, + MinH: 3, + }, + Mobile: &gqlmodel.DashboardEntryPos{ + W: 1, + H: 2, + X: 0, + Y: 0, + MinW: 1, + MinH: 2, + }, + }, + EntryType: gqlmodel.EntryTypePieChart, + }, + { + ID: 2, + Title: "other", + Total: false, + StatsSelection: &gqlmodel.StatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"abc"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: p(1), + }, + Pos: &gqlmodel.ResponsiveDashboardEntryPos{ + Desktop: &gqlmodel.DashboardEntryPos{ + W: 2, + H: 3, + X: 1, + Y: 3, + MinW: 2, + MinH: 3, + }, + Mobile: &gqlmodel.DashboardEntryPos{ + W: 1, + H: 2, + X: 0, + Y: 0, + MinW: 1, + MinH: 2, + }, + }, + EntryType: gqlmodel.EntryTypeBarChart, + }, + }, dashboards[0].Items) + + _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &bVal, &gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"kek"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: p(5), + }, &gqlmodel.InputResponsiveDashboardEntryPos{Desktop: &gqlmodel.InputDashboardEntryPos{ + H: 1, + W: 2, + X: 1, + Y: 3, + }}) + require.EqualError(t, err, "dashboard range does not exist") + + _, err = resolver.UpdateDashboardEntry(user1, 1, &chart, s("cool title"), &bVal, &gqlmodel.InputStatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"kek"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: p(xrange.ID), + }, &gqlmodel.InputResponsiveDashboardEntryPos{Desktop: &gqlmodel.InputDashboardEntryPos{ + H: 1, + W: 2, + X: 1, + Y: 3, + }}) + require.NoError(t, err) + + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Equal(t, []*gqlmodel.DashboardEntry{ + { + ID: 1, + Title: "cool title", + Total: false, + StatsSelection: &gqlmodel.StatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"kek"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: p(xrange.ID), + }, + Pos: &gqlmodel.ResponsiveDashboardEntryPos{ + Desktop: &gqlmodel.DashboardEntryPos{ + W: 2, + H: 3, + X: 1, + Y: 3, + MinW: 2, + MinH: 3, + }, + Mobile: &gqlmodel.DashboardEntryPos{ + W: 1, + H: 2, + X: 0, + Y: 0, + MinW: 1, + MinH: 2, + }, + }, + EntryType: gqlmodel.EntryTypePieChart, + }, + { + ID: 2, + Title: "other", + Total: false, + StatsSelection: &gqlmodel.StatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"abc"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: p(1), + }, + Pos: &gqlmodel.ResponsiveDashboardEntryPos{ + Desktop: &gqlmodel.DashboardEntryPos{ + W: 2, + H: 3, + X: 1, + Y: 3, + MinW: 2, + MinH: 3, + }, + Mobile: &gqlmodel.DashboardEntryPos{ + W: 1, + H: 2, + X: 0, + Y: 0, + MinW: 1, + MinH: 2, + }, + }, + EntryType: gqlmodel.EntryTypeBarChart, + }, + }, dashboards[0].Items) + + _, err = resolver.RemoveDashboardEntry(user2, 1) + require.EqualError(t, err, "dashboard does not exist") + + _, err = resolver.RemoveDashboardEntry(user1, 55) + require.EqualError(t, err, "entry does not exist") + + _, err = resolver.RemoveDashboardEntry(user1, 1) + require.NoError(t, err) + + dashboards, err = resolver.Dashboards(user1) + require.NoError(t, err) + require.Equal(t, []*gqlmodel.DashboardEntry{ + { + ID: 2, + Title: "other", + Total: false, + StatsSelection: &gqlmodel.StatsSelection{ + Interval: gqlmodel.StatsIntervalDaily, + Tags: []string{"abc"}, + ExcludeTags: nil, + IncludeTags: nil, + RangeID: p(1), + }, + Pos: &gqlmodel.ResponsiveDashboardEntryPos{ + Desktop: &gqlmodel.DashboardEntryPos{ + W: 2, + H: 3, + X: 1, + Y: 3, + MinW: 2, + MinH: 3, + }, + Mobile: &gqlmodel.DashboardEntryPos{ + W: 1, + H: 2, + X: 0, + Y: 0, + MinW: 1, + MinH: 2, + }, + }, + EntryType: gqlmodel.EntryTypeBarChart, + }, + }, dashboards[0].Items) +} + +func p(i int) *int { + return &i +} +func s(s string) *string { + return &s +} diff --git a/dashboard/entry/remove.go b/dashboard/entry/remove.go new file mode 100644 index 0000000..d1275cc --- /dev/null +++ b/dashboard/entry/remove.go @@ -0,0 +1,34 @@ +package entry + +import ( + "context" + + "github.com/traggo/server/dashboard/convert" + "github.com/traggo/server/dashboard/util" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// RemoveDashboardEntry removes a dashboard entry. +func (r *ResolverForEntry) RemoveDashboardEntry(ctx context.Context, id int) (*gqlmodel.DashboardEntry, error) { + + userID := auth.GetUser(ctx).ID + + entry, err := util.FindDashboardEntry(r.DB, id) + if err != nil { + return nil, err + } + + if _, err := util.FindDashboard(r.DB, userID, entry.DashboardID); err != nil { + return nil, err + } + + remove := r.DB.Delete(&model.DashboardEntry{}, id) + if remove.Error != nil { + return &gqlmodel.DashboardEntry{}, remove.Error + } + + return convert.ToExternalEntry(entry) +} diff --git a/dashboard/entry/tagcheck.go b/dashboard/entry/tagcheck.go new file mode 100644 index 0000000..1cde8dd --- /dev/null +++ b/dashboard/entry/tagcheck.go @@ -0,0 +1,33 @@ +package entry + +import ( + "fmt" + + "github.com/traggo/server/model" +) + +func tagsDuplicates(tags []model.DashboardTagFilter) error { + existingTags := make(map[model.DashboardTagFilter]struct{}) + + for _, tag := range tags { + if _, ok := existingTags[tag]; ok { + tagType := "exclude" + if tag.Include { + tagType = "include" + } + + return fmt.Errorf("%s tags: tag '%s' is present multiple times", tagType, tag.Key+":"+tag.StringValue) + } else { + copyTag := tag + copyTag.Include = !copyTag.Include + + if _, ok := existingTags[copyTag]; ok { + return fmt.Errorf("tag '%s' is present in both exclude tags and include tags", tag.Key+":"+tag.StringValue) + } + } + + existingTags[tag] = struct{}{} + } + + return nil +} diff --git a/dashboard/entry/update.go b/dashboard/entry/update.go new file mode 100644 index 0000000..b4b110a --- /dev/null +++ b/dashboard/entry/update.go @@ -0,0 +1,104 @@ +package entry + +import ( + "context" + "fmt" + "strings" + + "github.com/traggo/server/time" + + "github.com/traggo/server/model" + + "github.com/traggo/server/auth" + "github.com/traggo/server/dashboard/convert" + "github.com/traggo/server/dashboard/util" + "github.com/traggo/server/generated/gqlmodel" +) + +// UpdateDashboardEntry updates a dashboard entry. +func (r *ResolverForEntry) UpdateDashboardEntry(ctx context.Context, id int, entryType *gqlmodel.EntryType, title *string, total *bool, stats *gqlmodel.InputStatsSelection, pos *gqlmodel.InputResponsiveDashboardEntryPos) (*gqlmodel.DashboardEntry, error) { + userID := auth.GetUser(ctx).ID + + entry, err := util.FindDashboardEntry(r.DB, id) + if err != nil { + return nil, err + } + + if _, err := util.FindDashboard(r.DB, userID, entry.DashboardID); err != nil { + return nil, err + } + + if title != nil { + entry.Title = *title + } + + if total != nil { + entry.Total = *total + } + + tx := r.DB.Begin() + if err := tx.Error; err != nil { + return nil, err + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + panic(r) + } else if tx != nil { + tx.Rollback() + } + }() + + if stats != nil { + if stats.RangeID != nil { + if _, err := util.FindDashboardRange(tx, *stats.RangeID); err != nil { + return nil, err + } + entry.RangeID = *stats.RangeID + } else if stats.Range != nil { + entry.RangeID = model.NoRangeIDDefined + if err := time.Validate(stats.Range.From); err != nil { + return nil, fmt.Errorf("range from (%s) invalid: %s", stats.Range.From, err) + } + if err := time.Validate(stats.Range.To); err != nil { + return nil, fmt.Errorf("range to (%s) invalid: %s", stats.Range.To, err) + } + entry.RangeFrom = stats.Range.From + entry.RangeTo = stats.Range.To + } + + tagFilters := convert.TagFiltersToInternal(stats.ExcludeTags, false) + tagFilters = append(tagFilters, convert.TagFiltersToInternal(stats.IncludeTags, true)...) + + if err := tagsDuplicates(tagFilters); err != nil { + return nil, err + } + + if err := tx.Where("dashboard_entry_id = ?", id).Delete(new(model.DashboardTagFilter)).Error; err != nil { + return nil, fmt.Errorf("failed to update tag filters: %s", err) + } + + entry.TagFilters = tagFilters + entry.Keys = strings.Join(stats.Tags, ",") + entry.Interval = convert.InternalInterval(stats.Interval) + } + + if entryType != nil { + entry.Type = convert.InternalEntryType(*entryType) + } + + if err := convert.ApplyPos(&entry, pos); err != nil { + return &gqlmodel.DashboardEntry{}, err + } + + if err := tx.Save(entry).Error; err != nil { + return nil, err + } + + if err := tx.Commit().Error; err != nil { + return nil, err + } + + tx = nil + return convert.ToExternalEntry(entry) +} diff --git a/dashboard/get.go b/dashboard/get.go new file mode 100644 index 0000000..2c66eee --- /dev/null +++ b/dashboard/get.go @@ -0,0 +1,31 @@ +package dashboard + +import ( + "context" + + "github.com/traggo/server/dashboard/convert" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// Dashboards returns all dashboards. +func (r *ResolverForDashboard) Dashboards(ctx context.Context) ([]*gqlmodel.Dashboard, error) { + userID := auth.GetUser(ctx).ID + + dashboards := []model.Dashboard{} + + q := r.DB + q = q.Preload("Entries") + q = q.Preload("Entries.TagFilters") + q = q.Preload("Ranges") + + find := q.Where(&model.Dashboard{UserID: userID}).Find(&dashboards) + + if find.Error != nil { + return nil, find.Error + } + + return convert.ToExternalDashboards(dashboards) +} diff --git a/dashboard/remove.go b/dashboard/remove.go new file mode 100644 index 0000000..ed63213 --- /dev/null +++ b/dashboard/remove.go @@ -0,0 +1,28 @@ +package dashboard + +import ( + "context" + + "github.com/traggo/server/dashboard/convert" + "github.com/traggo/server/dashboard/util" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// RemoveDashboard removes a dashboard. +func (r *ResolverForDashboard) RemoveDashboard(ctx context.Context, id int) (*gqlmodel.Dashboard, error) { + userID := auth.GetUser(ctx).ID + + dashboard, err := util.FindDashboard(r.DB, userID, id) + if err != nil { + return nil, err + } + + if err := r.DB.Where(&model.Dashboard{UserID: userID, ID: id}).Delete(&model.Dashboard{}).Error; err != nil { + return &gqlmodel.Dashboard{}, err + } + + return convert.ToExternalDashboard(dashboard) +} diff --git a/dashboard/update.go b/dashboard/update.go new file mode 100644 index 0000000..27f460b --- /dev/null +++ b/dashboard/update.go @@ -0,0 +1,45 @@ +package dashboard + +import ( + "context" + + "github.com/traggo/server/dashboard/convert" + + "github.com/traggo/server/dashboard/util" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// UpdateDashboard updates a dashboard. +func (r *ResolverForDashboard) UpdateDashboard(ctx context.Context, id int, name string) (*gqlmodel.Dashboard, error) { + userID := auth.GetUser(ctx).ID + + dashboard, err := util.FindDashboard(r.DB, userID, id) + if err != nil { + return nil, err + } + + dashboard.Name = name + + save := r.DB.Save(dashboard) + + if save.Error != nil { + return nil, save.Error + } + + var entries []model.DashboardEntry + if err := r.DB.Where(&model.DashboardEntry{DashboardID: dashboard.ID}).Find(&entries).Error; err != nil { + return nil, err + } + dashboard.Entries = entries + + var ranges []model.DashboardRange + if err := r.DB.Where(&model.DashboardRange{DashboardID: dashboard.ID}).Find(&ranges).Error; err != nil { + return nil, err + } + dashboard.Ranges = ranges + + return convert.ToExternalDashboard(dashboard) +} diff --git a/dashboard/util/dashboard.go b/dashboard/util/dashboard.go new file mode 100644 index 0000000..2c307d1 --- /dev/null +++ b/dashboard/util/dashboard.go @@ -0,0 +1,41 @@ +package util + +import ( + "errors" + + "github.com/jinzhu/gorm" + "github.com/traggo/server/model" +) + +// FindDashboard finds a dashboard. +func FindDashboard(db *gorm.DB, userID, dashboardID int) (model.Dashboard, error) { + dashboard := model.Dashboard{} + find := db.Where(&model.Dashboard{UserID: userID, ID: dashboardID}).Find(&dashboard) + if find.RecordNotFound() { + return dashboard, errors.New("dashboard does not exist") + } + + return dashboard, find.Error +} + +// FindDashboardRange finds a dashboard range. +func FindDashboardRange(db *gorm.DB, rangeID int) (model.DashboardRange, error) { + dashboardRange := model.DashboardRange{} + find := db.Where(&model.DashboardRange{ID: rangeID}).Find(&dashboardRange) + if find.RecordNotFound() { + return dashboardRange, errors.New("dashboard range does not exist") + } + + return dashboardRange, find.Error +} + +// FindDashboardEntry finds a dashboard entry. +func FindDashboardEntry(db *gorm.DB, entryID int) (model.DashboardEntry, error) { + dashboardEntry := model.DashboardEntry{} + find := db.Where(&model.DashboardEntry{ID: entryID}).Find(&dashboardEntry) + if find.RecordNotFound() { + return dashboardEntry, errors.New("entry does not exist") + } + + return dashboardEntry, find.Error +} diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..f363ca1 --- /dev/null +++ b/database/database.go @@ -0,0 +1,57 @@ +package database + +import ( + "os" + "path/filepath" + + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/mysql" // enable the mysql dialect + _ "github.com/jinzhu/gorm/dialects/postgres" // enable the postgres dialect + _ "github.com/jinzhu/gorm/dialects/sqlite" // enable the sqlite3 dialect + "github.com/rs/zerolog/log" + "github.com/traggo/server/logger" + "github.com/traggo/server/model" +) + +var mkdirAll = os.MkdirAll + +// New creates a gorm instance. +func New(dialect, connection string) (*gorm.DB, error) { + createDirectoryIfSqlite(dialect, connection) + + db, err := gorm.Open(dialect, connection) + if err != nil { + return nil, err + } + db.LogMode(true) + db.SetLogger(&logger.DatabaseLogger{}) + + // We normally don't need that much connections, so we limit them. F.ex. mysql complains about + // "too many connections". + db.DB().SetMaxOpenConns(10) + + if dialect == "sqlite3" { + // We use the database connection inside the handlers from the http + // framework, therefore concurrent access occurs. Sqlite cannot handle + // concurrent writes, so we limit sqlite to one connection. + // see https://github.com/mattn/go-sqlite3/issues/274 + db.DB().SetMaxOpenConns(1) + db.Exec("PRAGMA foreign_keys = ON") + } + + log.Debug().Msg("Auto migrating schema's") + db.AutoMigrate(model.All()...) + + log.Debug().Msg("Database initialized") + return db, nil +} + +func createDirectoryIfSqlite(dialect string, connection string) { + if dialect == "sqlite3" { + if _, err := os.Stat(filepath.Dir(connection)); os.IsNotExist(err) { + if err := mkdirAll(filepath.Dir(connection), 0777); err != nil { + panic(err) + } + } + } +} diff --git a/database/database_test.go b/database/database_test.go new file mode 100644 index 0000000..119bf33 --- /dev/null +++ b/database/database_test.go @@ -0,0 +1,56 @@ +package database + +import ( + "errors" + "os" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/traggo/server/logger" +) + +func TestMain(m *testing.M) { + logger.Init(zerolog.WarnLevel) + os.Exit(m.Run()) +} + +func TestInvalidDialect(t *testing.T) { + _, err := New("asdf", "testdb.db") + assert.NotNil(t, err) +} + +func TestCreateSqliteFolder(t *testing.T) { + // ensure path not exists + os.RemoveAll("somepath") + + db, err := New("sqlite3", "somepath/testdb.db") + assert.Nil(t, err) + assert.DirExists(t, "somepath") + db.Close() + + assert.Nil(t, os.RemoveAll("somepath")) +} + +func TestWithAlreadyExistingSqliteFolder(t *testing.T) { + // ensure path not exists + os.RemoveAll("somepath") + os.MkdirAll("somepath", 0777) + + db, err := New("sqlite3", "somepath/testdb.db") + assert.Nil(t, err) + assert.DirExists(t, "somepath") + db.Close() + + assert.Nil(t, os.RemoveAll("somepath")) +} + +func TestPanicsOnMkdirError(t *testing.T) { + os.RemoveAll("somepath") + mkdirAll = func(path string, perm os.FileMode) error { + return errors.New("ERROR") + } + assert.Panics(t, func() { + New("sqlite3", "somepath/test.db") + }) +} diff --git a/device/create.go b/device/create.go new file mode 100644 index 0000000..f846302 --- /dev/null +++ b/device/create.go @@ -0,0 +1,83 @@ +package device + +import ( + "context" + "errors" + "time" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/auth/rand" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/user/password" +) + +var ( + timeNow = time.Now + randToken = rand.Token + comparePassword = password.ComparePassword + errUserPassWrong = errors.New("username/password combination does not exist") +) + +// Login creates a device. +func (r *ResolverForDevice) Login(ctx context.Context, username string, pass string, deviceName string, deviceType gqlmodel.DeviceType, cookie bool) (*gqlmodel.Login, error) { + + user := new(model.User) + find := r.DB.Where("name = ?", username).Find(user) + + if find.RecordNotFound() { + return nil, errUserPassWrong + } + + if !comparePassword(user.Pass, []byte(pass)) { + return nil, errUserPassWrong + } + + return r.createDeviceInternal(ctx, user, deviceName, deviceType, cookie) +} + +// CreateDevice creates a device. +func (r *ResolverForDevice) CreateDevice(ctx context.Context, deviceName string, deviceType gqlmodel.DeviceType) (*gqlmodel.Login, error) { + + user := auth.GetUser(ctx) + + return r.createDeviceInternal(ctx, user, deviceName, deviceType, false) +} + +func (r *ResolverForDevice) createDeviceInternal(ctx context.Context, user *model.User, deviceName string, deviceType gqlmodel.DeviceType, cookie bool) (*gqlmodel.Login, error) { + + token := randToken(20) + for !r.DB.Where("token = ?", token).Find(new(model.Device)).RecordNotFound() { + token = randToken(20) + } + + now := timeNow() + device := &model.Device{ + Token: token, + UserID: user.ID, + Name: deviceName, + Type: model.DeviceType(deviceType), + CreatedAt: now.UTC(), + ActiveAt: now.UTC(), + } + + if err := device.Type.Valid(); err != nil { + return nil, err + } + + if cookie { + auth.GetCreateSession(ctx)(token, device.Type.Seconds()) + } + + create := r.DB.Create(device) + + gqlUser := &gqlmodel.User{} + copier.Copy(gqlUser, user) + gqlDevice := &gqlmodel.Device{} + copier.Copy(gqlDevice, device) + return &gqlmodel.Login{ + User: gqlUser, + Device: gqlDevice, + Token: token}, create.Error +} diff --git a/device/create_test.go b/device/create_test.go new file mode 100644 index 0000000..19178dd --- /dev/null +++ b/device/create_test.go @@ -0,0 +1,271 @@ +package device + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +var ( + existingPassword = []byte{1} +) + +func TestGQL_Login_fails_userDoesNotExist(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + now := test.Time("2018-06-30T18:30:00+02:00") + timeDispose := fakeTime(now) + defer timeDispose() + + resolver := ResolverForDevice{DB: db.DB} + login, err := resolver.Login( + context.Background(), + "jmattheis", + "123", + "test", + gqlmodel.DeviceTypeLongExpiry, + false) + + assert.Nil(t, login) + assert.Equal(t, errUserPassWrong, err) + assertDeviceCount(t, db, 0) +} + +func TestGQL_Login_fails_wrongPass(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.Create(&model.User{ + Name: "jmattheis", + ID: 1, + Pass: existingPassword, + }) + pwDispose := fakePassword() + defer pwDispose() + + now := test.Time("2018-06-30T18:30:00+02:00") + timeDispose := fakeTime(now) + defer timeDispose() + + resolver := ResolverForDevice{DB: db.DB} + login, err := resolver.Login( + context.Background(), + "jmattheis", + "123", + "test", + gqlmodel.DeviceTypeLongExpiry, + false) + + assert.Nil(t, login) + assert.Equal(t, errUserPassWrong, err) + assertDeviceCount(t, db, 0) +} + +func TestGQL_Login_succeeds(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.Create(&model.User{ + Name: "jmattheis", + ID: 1, + Pass: existingPassword, + }) + pwDispose := fakePassword() + defer pwDispose() + + tokenDispose := fakeToken("firstToken") + defer tokenDispose() + + now := test.Time("2018-06-30T18:30:00+02:00") + timeDispose := fakeTime(now) + defer timeDispose() + + resolver := ResolverForDevice{DB: db.DB} + login, err := resolver.Login( + context.Background(), + "jmattheis", + "unicorn", + "test", + gqlmodel.DeviceTypeLongExpiry, + false) + + assert.Nil(t, err) + + expected := &gqlmodel.Login{ + Token: "firstToken", + User: &gqlmodel.User{Admin: false, ID: 1, Name: "jmattheis"}, + Device: &gqlmodel.Device{ + ID: 1, + Name: "test", + Type: gqlmodel.DeviceTypeLongExpiry, + CreatedAt: model.Time(now.UTC()), + ActiveAt: model.Time(now.UTC()), + }, + } + assert.Equal(t, expected, login) + assertDeviceCount(t, db, 1) +} + +func TestGQL_CreateDevice_succeeds(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := &model.User{ + Name: "jmattheis", + ID: 1, + Pass: existingPassword, + } + db.Create(user) + pwDispose := fakePassword() + defer pwDispose() + + tokenDispose := fakeToken("firstToken") + defer tokenDispose() + + now := test.Time("2018-06-30T18:30:00+02:00") + timeDispose := fakeTime(now) + defer timeDispose() + + resolver := ResolverForDevice{DB: db.DB} + login, err := resolver.CreateDevice( + auth.WithUser(context.Background(), user), + "test", + gqlmodel.DeviceTypeLongExpiry) + + assert.Nil(t, err) + + expected := &gqlmodel.Login{ + Token: "firstToken", + User: &gqlmodel.User{Admin: false, ID: 1, Name: "jmattheis"}, + Device: &gqlmodel.Device{ + ID: 1, + Name: "test", + Type: gqlmodel.DeviceTypeLongExpiry, + CreatedAt: model.Time(now.UTC()), + ActiveAt: model.Time(now.UTC()), + }, + } + assert.Equal(t, expected, login) + assertDeviceCount(t, db, 1) +} + +func TestGQL_Login_setsCookie(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.Create(&model.User{ + Name: "jmattheis", + ID: 1, + Pass: existingPassword, + }) + pwDispose := fakePassword() + defer pwDispose() + + tokenDispose := fakeToken("firstToken") + defer tokenDispose() + + now := test.Time("2018-06-30T18:30:00+02:00") + timeDispose := fakeTime(now) + defer timeDispose() + + resolver := ResolverForDevice{DB: db.DB} + + createSessionToken := "" + createSessionAge := 0 + + _, err := resolver.Login( + auth.WithCreateSession(context.Background(), func(token string, age int) { + createSessionToken = token + createSessionAge = age + }), + "jmattheis", + "unicorn", + "test", + gqlmodel.DeviceTypeShortExpiry, + true) + + assert.Nil(t, err) + + assert.Equal(t, "firstToken", createSessionToken) + assert.Equal(t, 60*60, createSessionAge) + assertDeviceCount(t, db, 1) +} + +func TestGQL_Login_succeeds_withExistingToken(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.Create(&model.User{ + Name: "jmattheis", + ID: 1, + Pass: existingPassword, + }) + db.Create(&model.Device{ + Name: "test", + ID: 1, + Token: "firstToken", + UserID: 1, + }) + pwDispose := fakePassword() + defer pwDispose() + + tokenDispose := fakeToken("firstToken", "secondToken") + defer tokenDispose() + + now := test.Time("2018-06-30T18:30:00+02:00") + timeDispose := fakeTime(now) + defer timeDispose() + + resolver := ResolverForDevice{DB: db.DB} + login, err := resolver.Login( + context.Background(), + "jmattheis", + "unicorn", + "test", + gqlmodel.DeviceTypeShortExpiry, + false) + + assert.Nil(t, err) + + assert.Equal(t, "secondToken", login.Token) + assertDeviceCount(t, db, 2) +} + +func fakePassword() func() { + old := comparePassword + comparePassword = func(hashedPassword, password []byte) bool { + return bytes.Equal(password, []byte("unicorn")) && bytes.Equal(hashedPassword, existingPassword) + } + return func() { + comparePassword = old + } +} + +func fakeTime(t time.Time) func() { + old := timeNow + timeNow = func() time.Time { + return t + } + return func() { + timeNow = old + } +} + +func fakeToken(token ...string) func() { + old := randToken + remaining := token + randToken = func(n int) string { + if len(remaining) > 0 { + used := remaining[0] + remaining = remaining[1:] + return used + } + panic("oops no token specified") + } + return func() { + randToken = old + } +} diff --git a/device/current.go b/device/current.go new file mode 100644 index 0000000..ff190b2 --- /dev/null +++ b/device/current.go @@ -0,0 +1,20 @@ +package device + +import ( + "context" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" +) + +// CurrentDevice returns the current device. +func (r *ResolverForDevice) CurrentDevice(ctx context.Context) (*gqlmodel.Device, error) { + device := auth.GetDevice(ctx) + if device == nil { + return nil, nil + } + var result gqlmodel.Device + copier.Copy(&result, device) + return &result, nil +} diff --git a/device/current_test.go b/device/current_test.go new file mode 100644 index 0000000..cc22488 --- /dev/null +++ b/device/current_test.go @@ -0,0 +1,53 @@ +package device + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestGQL_CurrentDevice_withDevice(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + device := &model.Device{ + ID: 2, + Name: "Browser", + Token: "abcd", + UserID: 2, + CreatedAt: test.Time("2004-06-30T18:30:00Z"), + ActiveAt: test.Time("2015-06-30T18:30:00Z"), + Type: model.TypeNoExpiry, + } + + resolver := ResolverForDevice{DB: db.DB} + result, err := resolver.CurrentDevice(fake.Device(device)) + + require.Nil(t, err) + expected := &gqlmodel.Device{ + ID: 2, + Name: "Browser", + CreatedAt: test.ModelTimeUTC("2004-06-30T18:30:00Z"), + ActiveAt: test.ModelTimeUTC("2015-06-30T18:30:00Z"), + Type: gqlmodel.DeviceTypeNoExpiry, + } + + require.Equal(t, expected, result) +} + +func TestGQL_CurrentDevice_noDevice(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + resolver := ResolverForDevice{DB: db.DB} + result, err := resolver.CurrentDevice(context.Background()) + + require.Nil(t, err) + + require.Nil(t, result) +} diff --git a/device/deviceresolver.go b/device/deviceresolver.go new file mode 100644 index 0000000..f35927c --- /dev/null +++ b/device/deviceresolver.go @@ -0,0 +1,8 @@ +package device + +import "github.com/jinzhu/gorm" + +// ResolverForDevice resolves device specific things. +type ResolverForDevice struct { + DB *gorm.DB +} diff --git a/device/get.go b/device/get.go new file mode 100644 index 0000000..e5ca19c --- /dev/null +++ b/device/get.go @@ -0,0 +1,20 @@ +package device + +import ( + "context" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// Devices returns all devices. +func (r *ResolverForDevice) Devices(ctx context.Context) ([]*gqlmodel.Device, error) { + user := auth.GetUser(ctx) + var devices []model.Device + find := r.DB.Where(&model.Device{UserID: user.ID}).Order("active_at DESC").Find(&devices) + var result []*gqlmodel.Device + copier.Copy(&result, &devices) + return result, find.Error +} diff --git a/device/get_test.go b/device/get_test.go new file mode 100644 index 0000000..0d1e38a --- /dev/null +++ b/device/get_test.go @@ -0,0 +1,51 @@ +package device + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestGQL_Devices(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(1) + db.User(2) + db.Create(&model.Device{ + ID: 1, + Name: "Android", + Token: "abc", + UserID: 1, + CreatedAt: test.Time("2009-06-30T18:30:00Z"), + ActiveAt: test.Time("2018-06-30T18:30:00Z"), + Type: model.TypeNoExpiry, + }) + db.Create(&model.Device{ + ID: 2, + Name: "Browser", + Token: "abcd", + UserID: 2, + CreatedAt: test.Time("2004-06-30T18:30:00Z"), + ActiveAt: test.Time("2015-06-30T18:30:00Z"), + Type: model.TypeNoExpiry, + }) + + resolver := ResolverForDevice{DB: db.DB} + devices, err := resolver.Devices(fake.User(1)) + + require.Nil(t, err) + expected := []*gqlmodel.Device{ + { + ID: 1, + Name: "Android", + CreatedAt: test.ModelTimeUTC("2009-06-30T18:30:00Z"), + ActiveAt: test.ModelTimeUTC("2018-06-30T18:30:00Z"), + Type: gqlmodel.DeviceTypeNoExpiry, + }, + } + require.Equal(t, expected, devices) +} diff --git a/device/remove.go b/device/remove.go new file mode 100644 index 0000000..c5d3d0e --- /dev/null +++ b/device/remove.go @@ -0,0 +1,24 @@ +package device + +import ( + "context" + "fmt" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// RemoveDevice removes a device +func (r *ResolverForDevice) RemoveDevice(ctx context.Context, id int) (*gqlmodel.Device, error) { + device := model.Device{ID: id} + if r.DB.Where(&model.Device{UserID: auth.GetUser(ctx).ID}).Find(&device).RecordNotFound() { + return nil, fmt.Errorf("device not found") + } + + remove := r.DB.Delete(&device) + gqlDevice := &gqlmodel.Device{} + copier.Copy(gqlDevice, &device) + return gqlDevice, remove.Error +} diff --git a/device/remove_current.go b/device/remove_current.go new file mode 100644 index 0000000..0519df5 --- /dev/null +++ b/device/remove_current.go @@ -0,0 +1,18 @@ +package device + +import ( + "context" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" +) + +// RemoveCurrentDevice removes the current authenticated device +func (r *ResolverForDevice) RemoveCurrentDevice(ctx context.Context) (*gqlmodel.Device, error) { + device := auth.GetDevice(ctx) + remove := r.DB.Delete(device) + gqlDevice := &gqlmodel.Device{} + copier.Copy(gqlDevice, device) + return gqlDevice, remove.Error +} diff --git a/device/remove_current_test.go b/device/remove_current_test.go new file mode 100644 index 0000000..0a5dbe0 --- /dev/null +++ b/device/remove_current_test.go @@ -0,0 +1,41 @@ +package device + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestGQL_RemoveCurrentDevice_succeeds_removesDevice(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(1) + device := &model.Device{ + ID: 55, + Name: "Android", + Token: "abc", + UserID: 1, + CreatedAt: test.Time("2009-06-30T18:30:00Z"), + ActiveAt: test.Time("2018-06-30T18:30:00Z"), + Type: model.TypeNoExpiry, + } + db.Create(device) + resolver := ResolverForDevice{DB: db.DB} + gqlDevice, err := resolver.RemoveCurrentDevice(fake.Device(device)) + require.Nil(t, err) + + expected := &gqlmodel.Device{ + ID: 55, + Name: "Android", + CreatedAt: test.ModelTimeUTC("2009-06-30T18:30:00Z"), + ActiveAt: test.ModelTimeUTC("2018-06-30T18:30:00Z"), + Type: gqlmodel.DeviceTypeNoExpiry, + } + + require.Equal(t, expected, gqlDevice) + assertDeviceCount(t, db, 0) +} diff --git a/device/remove_test.go b/device/remove_test.go new file mode 100644 index 0000000..e88636d --- /dev/null +++ b/device/remove_test.go @@ -0,0 +1,58 @@ +package device + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestGQL_RemoveDevice_succeeds_removesDevice(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(1) + db.Create(&model.Device{ + ID: 55, + Name: "Android", + Token: "abc", + UserID: 1, + CreatedAt: test.Time("2009-06-30T18:30:00+02:00"), + ActiveAt: test.Time("2018-06-30T18:30:00+02:00"), + Type: model.TypeNoExpiry, + }) + resolver := ResolverForDevice{DB: db.DB} + _, err := resolver.RemoveDevice(fake.User(1), 55) + require.Nil(t, err) + assertDeviceCount(t, db, 0) +} + +func TestGQL_RemoveDevice_fails_notExistingDevice(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(1) + + resolver := ResolverForDevice{DB: db.DB} + _, err := resolver.RemoveDevice(fake.User(1), 55) + require.EqualError(t, err, "device not found") +} + +func TestGQL_RemoveDevice_fails_notPermission(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(1) + db.User(2) + db.Create(&model.Device{ + ID: 55, + Name: "Android", + Token: "abc", + UserID: 2, + CreatedAt: test.Time("2009-06-30T18:30:00+02:00"), + ActiveAt: test.Time("2018-06-30T18:30:00+02:00"), + Type: model.TypeNoExpiry, + }) + resolver := ResolverForDevice{DB: db.DB} + _, err := resolver.RemoveDevice(fake.User(1), 55) + require.EqualError(t, err, "device not found") +} diff --git a/device/update.go b/device/update.go new file mode 100644 index 0000000..a2b021b --- /dev/null +++ b/device/update.go @@ -0,0 +1,29 @@ +package device + +import ( + "context" + "errors" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// UpdateDevice updates a device. +func (r *ResolverForDevice) UpdateDevice(ctx context.Context, id int, name string, deviceType gqlmodel.DeviceType) (*gqlmodel.Device, error) { + device := new(model.Device) + if r.DB.Where("user_id = ?", auth.GetUser(ctx).ID).Find(device, id).RecordNotFound() { + return nil, errors.New("device not found") + } + + device.Name = name + device.Type = model.DeviceType(deviceType) + if err := device.Type.Valid(); err != nil { + return nil, err + } + update := r.DB.Save(device) + gqlDevice := &gqlmodel.Device{} + copier.Copy(gqlDevice, device) + return gqlDevice, update.Error +} diff --git a/device/update_test.go b/device/update_test.go new file mode 100644 index 0000000..2686e26 --- /dev/null +++ b/device/update_test.go @@ -0,0 +1,94 @@ +package device + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestGQL_UpdateDevice_succeeds_updatesDevice(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.Create(&model.User{ + Name: "jmattheis", + ID: 1, + Admin: true, + }) + db.Create(&model.Device{ + Name: "old name", + ID: 1, + UserID: 1, + CreatedAt: test.Time("2009-06-30T18:30:00Z"), + ActiveAt: test.Time("2018-06-30T18:30:00Z"), + Type: model.TypeNoExpiry, + }) + + resolver := ResolverForDevice{DB: db.DB} + device, err := resolver.UpdateDevice(fake.User(1), 1, "updated name", gqlmodel.DeviceTypeShortExpiry) + require.Nil(t, err) + + expected := &gqlmodel.Device{ + Name: "updated name", + ID: 1, + CreatedAt: test.ModelTimeUTC("2009-06-30T18:30:00Z"), + ActiveAt: test.ModelTimeUTC("2018-06-30T18:30:00Z"), + Type: gqlmodel.DeviceTypeShortExpiry, + } + require.Equal(t, expected, device) + assertDeviceCount(t, db, 1) + assertDeviceExist(t, db, model.Device{ + Name: "updated name", + ID: 1, + UserID: 1, + CreatedAt: test.Time("2009-06-30T18:30:00Z"), + ActiveAt: test.Time("2018-06-30T18:30:00Z"), + Type: model.TypeShortExpiry, + }) +} + +func TestGQL_UpdateDevice_fails_notExistingDevice(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.Create(&model.User{ + Name: "jmattheis", + ID: 1, + Admin: true, + }) + resolver := ResolverForDevice{DB: db.DB} + _, err := resolver.UpdateDevice(fake.User(1), 3, "tst", gqlmodel.DeviceTypeShortExpiry) + require.EqualError(t, err, "device not found") + + assertDeviceCount(t, db, 0) +} + +func TestGQL_UpdateDevice_fails_noPermissions(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.Create(&model.User{ + Name: "jmattheis", + ID: 1, + Admin: true, + }) + db.Create(&model.User{ + Name: "broderp", + ID: 2, + Admin: true, + }) + db.Create(&model.Device{ + Name: "old name", + ID: 66, + UserID: 2, + CreatedAt: test.Time("2009-06-30T18:30:00Z"), + ActiveAt: test.Time("2018-06-30T18:30:00Z"), + Type: model.TypeNoExpiry, + }) + resolver := ResolverForDevice{DB: db.DB} + _, err := resolver.UpdateDevice(fake.User(1), 66, "tst", gqlmodel.DeviceTypeNoExpiry) + require.EqualError(t, err, "device not found") + + assertDeviceCount(t, db, 1) +} diff --git a/device/utils_test.go b/device/utils_test.go new file mode 100644 index 0000000..ff067f1 --- /dev/null +++ b/device/utils_test.go @@ -0,0 +1,23 @@ +package device + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +func assertDeviceExist(t *testing.T, db *test.Database, expected model.Device) { + foundDevice := new(model.Device) + find := db.Find(foundDevice, expected.ID) + require.Nil(t, find.Error) + require.NotNil(t, foundDevice) + require.Equal(t, expected, *foundDevice) +} + +func assertDeviceCount(t *testing.T, db *test.Database, expected int) { + count := new(int) + db.Model(new(model.Device)).Count(count) + require.Equal(t, expected, *count) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..026de8f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: "3.9" +services: + traggo: + build: + context: . + dockerfile: docker/Dockerfile.dev + environment: + TRAGGO_DEFAULT_USER_NAME: "admin" + TRAGGO_DEFAULT_USER_PASS: "admin" + + TRAGGO_LOG_LEVEL: debug + ports: + - 3030:3030 + volumes: + - .traggodata:/opt/traggo/data diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..4673502 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,6 @@ +FROM scratch +LABEL org.opencontainers.image.source="https://github.com/traggo/server" +WORKDIR /opt/traggo +COPY traggo /opt/traggo +EXPOSE 3030 +ENTRYPOINT ["./traggo"] diff --git a/docker/Dockerfile.build b/docker/Dockerfile.build new file mode 100644 index 0000000..769f2b2 --- /dev/null +++ b/docker/Dockerfile.build @@ -0,0 +1 @@ +FROM ghcr.io/goreleaser/goreleaser-cross:v1.27.0 diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev new file mode 100644 index 0000000..3061f65 --- /dev/null +++ b/docker/Dockerfile.dev @@ -0,0 +1,15 @@ +FROM golang:1.18.1 as builder + +RUN apt-get update && apt-get install --yes nodejs npm && npm install --global yarn + +WORKDIR /app +COPY . . + +ENV GO111MODULE=on +RUN make download-tools install generate build-bin-local + +FROM scratch +WORKDIR /opt/traggo +COPY --from=builder /app/build/traggo-server /opt/traggo/traggo +EXPOSE 3030 +ENTRYPOINT ["./traggo"] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f0912ec --- /dev/null +++ b/go.mod @@ -0,0 +1,44 @@ +module github.com/traggo/server + +require ( + github.com/99designs/gqlgen v0.17.85 + github.com/gorilla/mux v1.8.1 + github.com/jinzhu/copier v0.4.0 + github.com/jinzhu/gorm v1.9.2 + github.com/jmattheis/go-timemath v1.0.1 + github.com/joho/godotenv v1.5.1 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/magiconair/properties v1.8.10 + github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 + github.com/rs/zerolog v1.34.0 + github.com/stretchr/testify v1.11.1 + github.com/vektah/gqlparser/v2 v2.5.31 + golang.org/x/crypto v0.46.0 +) + +require ( + cloud.google.com/go v0.34.0 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f // indirect + github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect + github.com/go-sql-driver/mysql v1.4.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gofrs/uuid v3.1.0+incompatible // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect + github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3 // indirect + github.com/lib/pq v1.0.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.10.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sosodev/duration v1.3.1 // indirect + golang.org/x/sys v0.39.0 // indirect + google.golang.org/appengine v1.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +go 1.24.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8eb0615 --- /dev/null +++ b/go.sum @@ -0,0 +1,103 @@ +cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/99designs/gqlgen v0.17.85 h1:EkGx3U2FDcxQm8YDLQSpXIAVmpDyZ3IcBMOJi2nH1S0= +github.com/99designs/gqlgen v0.17.85/go.mod h1:yvs8s0bkQlRfqg03YXr3eR4OQUowVhODT/tHzCXnbOU= +github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= +github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f h1:WH0w/R4Yoey+04HhFxqZ6VX6I0d7RMyw5aXQ9UTvQPs= +github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA= +github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/gorm v1.9.2 h1:lCvgEaqe/HVE+tjAR2mt4HbbHAZsQOv3XAZiEZV37iw= +github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= +github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k= +github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3 h1:xvj06l8iSwiWpYgm8MbPp+naBg+pwfqmdXabzqPCn/8= +github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= +github.com/jmattheis/go-timemath v1.0.1 h1:AY8bIDpD35nlWC5rhG+V4nLQ0OanU9omBcx0ahApT68= +github.com/jmattheis/go-timemath v1.0.1/go.mod h1:/zMgCjpruOIsZPBFAPJ3F1ei09hAywu3PT9Eyd8tmaA= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k= +github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gqlgen.yml b/gqlgen.yml new file mode 100644 index 0000000..6a22bfe --- /dev/null +++ b/gqlgen.yml @@ -0,0 +1,15 @@ +schema: schema.graphql + +exec: + filename: generated/gqlschema/generated.go + package: gqlschema + +model: + filename: generated/gqlmodel/generated.go + package: gqlmodel + +models: + Time: + model: github.com/traggo/server/model.Time + +struct_tag: json \ No newline at end of file diff --git a/graphql/directive.go b/graphql/directive.go new file mode 100644 index 0000000..cf67ca7 --- /dev/null +++ b/graphql/directive.go @@ -0,0 +1,13 @@ +package graphql + +import ( + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlschema" +) + +// NewDirective creates a new directive. +func NewDirective() gqlschema.DirectiveRoot { + return gqlschema.DirectiveRoot{ + HasRole: auth.HasRole(), + } +} diff --git a/graphql/handler.go b/graphql/handler.go new file mode 100644 index 0000000..9981eae --- /dev/null +++ b/graphql/handler.go @@ -0,0 +1,32 @@ +package graphql + +import ( + "net/http" + "strings" + + "github.com/99designs/gqlgen/handler" + "github.com/traggo/server/generated/gqlschema" + "github.com/traggo/server/logger" +) + +// Handler combines graphql handler and playground handler. +func Handler(endpoint string, resolvers gqlschema.ResolverRoot, directives gqlschema.DirectiveRoot) http.HandlerFunc { + gqlHandler := handler.GraphQL(gqlschema.NewExecutableSchema(gqlschema.Config{ + Resolvers: resolvers, + Directives: directives, + }), handler.RequestMiddleware(logger.GQLLog())) + playground := handler.Playground("Traggo Playground", endpoint) + + return func(writer http.ResponseWriter, request *http.Request) { + if acceptHTMLAndNotJSON(request) { + playground.ServeHTTP(writer, request) + } else { + gqlHandler.ServeHTTP(writer, request) + } + } +} + +func acceptHTMLAndNotJSON(request *http.Request) bool { + val := request.Header.Get("Accept") + return strings.Contains(val, "text/html") && !strings.Contains(val, "application/json") +} diff --git a/graphql/handler_test.go b/graphql/handler_test.go new file mode 100644 index 0000000..4302dca --- /dev/null +++ b/graphql/handler_test.go @@ -0,0 +1,42 @@ +package graphql + +import ( + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +func TestHandler_jsonOverHtml(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + resolver := NewResolver(db.DB, 4, model.Version{}) + handler := Handler("/gql", resolver, NewDirective()) + req := httptest.NewRequest("GET", "/gql?query="+url.QueryEscape("query {currentUser {name}}"), strings.NewReader("")) + req.Header.Set("Accept", "text/html;application/json") + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + require.Equal(t, "application/graphql-response+json", recorder.Header().Get("Content-Type")) + require.JSONEq(t, ` +{ + "data": { "currentUser": null } +} +`, recorder.Body.String()) +} + +func TestHandler_htmlIfNotJson(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + resolver := NewResolver(db.DB, 4, model.Version{}) + handler := Handler("/gql", resolver, NewDirective()) + req := httptest.NewRequest("get", "/gql", strings.NewReader("")) + req.Header.Set("Accept", "text/html;application/xml") + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + require.Contains(t, recorder.Body.String(), "Traggo Playground") +} diff --git a/graphql/resolver.go b/graphql/resolver.go new file mode 100644 index 0000000..aac2a3c --- /dev/null +++ b/graphql/resolver.go @@ -0,0 +1,71 @@ +package graphql + +import ( + "context" + + "github.com/traggo/server/dashboard" + "github.com/traggo/server/setting" + + "github.com/jinzhu/copier" + "github.com/jinzhu/gorm" + "github.com/traggo/server/device" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/generated/gqlschema" + "github.com/traggo/server/model" + "github.com/traggo/server/statistics" + "github.com/traggo/server/tag" + "github.com/traggo/server/timespan" + "github.com/traggo/server/user" +) + +// NewResolver combines all resolvers to a resolver root. +func NewResolver(db *gorm.DB, passStrength int, version model.Version) gqlschema.ResolverRoot { + return &resolver{ + ResolverForUser: user.ResolverForUser{ + DB: db, + PassStrength: passStrength, + }, + ResolverForTag: tag.ResolverForTag{ + DB: db, + }, + ResolverForDevice: device.ResolverForDevice{ + DB: db, + }, + ResolverForTimeSpan: timespan.ResolverForTimeSpan{ + DB: db, + }, + ResolverForStatistics: statistics.ResolverForStatistics{ + DB: db, + }, + ResolverForSettings: setting.ResolverForSettings{ + DB: db, + }, + ResolverForDashboard: dashboard.NewResolverForDashboard(db), + version: version, + } +} + +type resolver struct { + user.ResolverForUser + tag.ResolverForTag + device.ResolverForDevice + timespan.ResolverForTimeSpan + statistics.ResolverForStatistics + version model.Version + setting.ResolverForSettings + dashboard.ResolverForDashboard +} + +func (r *resolver) RootMutation() gqlschema.RootMutationResolver { + return r +} + +func (r *resolver) RootQuery() gqlschema.RootQueryResolver { + return r +} + +func (r *resolver) Version(ctx context.Context) (*gqlmodel.Version, error) { + gql := &gqlmodel.Version{} + copier.Copy(gql, r.version) + return gql, nil +} diff --git a/graphql/resolver_test.go b/graphql/resolver_test.go new file mode 100644 index 0000000..7d6d699 --- /dev/null +++ b/graphql/resolver_test.go @@ -0,0 +1,22 @@ +package graphql + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +func TestNewResolver_doesNotThrow(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + resolver := NewResolver(db.DB, 4, model.Version{Name: "oops", BuildDate: "date", Commit: "aeu"}) + resolver.RootMutation() + resolver.RootQuery() + version, e := resolver.RootQuery().Version(context.Background()) + assert.NoError(t, e) + assert.Equal(t, &gqlmodel.Version{Name: "oops", BuildDate: "date", Commit: "aeu"}, version) +} diff --git a/hack/datagen/datagen.go b/hack/datagen/datagen.go new file mode 100644 index 0000000..8c07809 --- /dev/null +++ b/hack/datagen/datagen.go @@ -0,0 +1,224 @@ +package main + +import ( + "flag" + "fmt" + "math/rand" + gotime "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/traggo/server/database" + "github.com/traggo/server/logger" + "github.com/traggo/server/model" + "github.com/traggo/server/time" + "github.com/traggo/server/user/password" +) + +func main() { + logger.Init(zerolog.InfoLevel) + rand.Seed(0) + var from string + flag.StringVar(&from, "from", "now/y", "from") + var to string + flag.StringVar(&to, "to", "now", "to") + + db, err := database.New("sqlite3", "data.db") + noErr(err) + defer db.Close() + + log.Info().Msg("Creating User ...") + user := &model.User{ + Name: "admin", + Pass: password.CreatePassword("admin", 10), + Admin: true, + } + noErr(db.Create(user).Error) + log.Info().Msg("... Done") + uID := user.ID + + log.Info().Msg("Creating Tags ...") + + db.Create(&model.TagDefinition{ + Key: "type", + UserID: uID, + Color: "#fff", + }) + db.Create(&model.TagDefinition{ + Key: "issue", + UserID: uID, + Color: "#fff", + }) + db.Create(&model.TagDefinition{ + Key: "meeting", + UserID: uID, + Color: "#fff", + }) + db.Create(&model.TagDefinition{ + Key: "with", + UserID: uID, + Color: "#fff", + }) + db.Create(&model.TagDefinition{ + Key: "misc", + UserID: uID, + Color: "#fff", + }) + db.Create(&model.TagDefinition{ + Key: "proj", + UserID: uID, + Color: "#fff", + }) + log.Info().Msg("... Done") + + now := gotime.Now().UTC() + fromTime, err := time.ParseTime(now, from, true, gotime.Monday) + noErr(err) + toTime, err := time.ParseTime(now, to, false, gotime.Sunday) + noErr(err) + + buckets := []bucket{ + { + TimeSpans: generateSupport(), + weight: 100, + }, + { + TimeSpans: generateMeeting(), + weight: 30, + }, + { + TimeSpans: generateIssueType("gotify", "review"), + weight: 100, + Active: 6, + }, + { + TimeSpans: generateIssueType("gotify", "work"), + weight: 100, + Active: 6, + }, + { + TimeSpans: generateIssueType("traggo", "review"), + weight: 100, + Active: 6, + }, + { + TimeSpans: generateIssueType("traggo", "work"), + weight: 100, + Active: 6, + }, + } + + sum := 0 + for _, bucket := range buckets { + sum += bucket.weight + } + + currentNow := fromTime + + log.Info().Msg("Creating TimeSpans ...") + for i := 1; currentNow.Before(toTime); i++ { + selected := randInt(0, sum) + current := 0 + var bucket bucket + for _, bucket = range buckets { + current += bucket.weight + if current > selected { + break + } + } + + duration := gotime.Minute * gotime.Duration(randInt(1, 150)) + + ts := bucket.TimeSpans[randInt(0, len(bucket.TimeSpans))] + end := currentNow.Add(duration) + db.Create(&model.TimeSpan{ + Tags: ts.Tags, + UserID: uID, + StartUserTime: currentNow, + StartUTC: currentNow, + EndUserTime: &end, + EndUTC: &end, + OffsetUTC: 0, + }) + currentNow = end.Add(gotime.Minute * gotime.Duration(randInt(1, 20))) + if i%500 == 0 { + log.Info().Msgf("Inserted %d ...", i) + } + } + log.Info().Msg("... Done") +} + +func generateIssueType(proj, t string) []timeSpan { + numbers := []int{} + for i := 1; i <= 1000; i++ { + numbers = append(numbers, i) + } + + result := []timeSpan{} + for _, number := range numbers { + result = append(result, timeSpan{ + Tags: []model.TimeSpanTag{ + {Key: "proj", StringValue: proj}, + {Key: "type", StringValue: t}, + {Key: "issue", StringValue: fmt.Sprintf("%X-%d", proj, number)}, + }, + Runtime: gotime.Hour * 24 * 12, + }) + } + return result +} + +func generateMeeting() []timeSpan { + names := []string{"weekly", "retro", "company", "team", "feature"} + + result := []timeSpan{} + for _, name := range names { + result = append(result, timeSpan{ + Tags: []model.TimeSpanTag{ + {Key: "type", StringValue: "meeting"}, + {Key: "meeting", StringValue: name}, + }, + Runtime: gotime.Duration(-1), + }) + } + return result +} + +func generateSupport() []timeSpan { + names := []string{"Tom", "Jerry", "Nico", "Anton", "Noob", "Alex", "Bob", "jmattheis", "nicories"} + + result := []timeSpan{} + for _, name := range names { + result = append(result, timeSpan{ + Tags: []model.TimeSpanTag{ + {Key: "type", StringValue: "support"}, + {Key: "with", StringValue: name}, + }, + Runtime: gotime.Duration(-1), + }) + } + return result +} + +type bucket struct { + TimeSpans []timeSpan + Active int + CurrentlyActive []timeSpan + weight int +} + +type timeSpan struct { + ActiveSince gotime.Time + Runtime gotime.Duration + Tags []model.TimeSpanTag +} + +func noErr(err error) { + if err != nil { + panic(err) + } +} + +func randInt(min int, max int) int { + return min + rand.Intn(max-min) +} diff --git a/logger/database.go b/logger/database.go new file mode 100644 index 0000000..2ce3ddb --- /dev/null +++ b/logger/database.go @@ -0,0 +1,69 @@ +package logger + +import ( + "fmt" + "reflect" + "regexp" + "time" + + "github.com/rs/zerolog/log" +) + +var ( + sqlRegexp = regexp.MustCompile(`\?`) +) + +// DatabaseLogger logs sql queries +type DatabaseLogger struct { +} + +// Print pretty prints the gorm.DB log values +// Mostly copied from https://github.com/jinzhu/gorm/blob/master/logger.go +func (l *DatabaseLogger) Print(values ...interface{}) { + if len(values) > 1 { + var ( + sql string + formattedValues []string + level = values[0] + ) + + if level == "sql" { + for _, value := range values[4].([]interface{}) { + indirectValue := reflect.Indirect(reflect.ValueOf(value)) + if indirectValue.IsValid() { + value = indirectValue.Interface() + if t, ok := value.(time.Time); ok { + formattedValues = append(formattedValues, fmt.Sprintf("'%v'", t.Format("2006-01-02 15:04:05"))) + } else if _, ok := value.([]byte); ok { + formattedValues = append(formattedValues, "''") + } else { + formattedValues = append(formattedValues, fmt.Sprintf("'%v'", value)) + } + } else { + formattedValues = append(formattedValues, "NULL") + } + } + + formattedValuesLength := len(formattedValues) + for index, value := range sqlRegexp.Split(values[3].(string), -1) { + sql += value + if index < formattedValuesLength { + sql += formattedValues[index] + } + } + + log.Debug().Str("took", values[2].(time.Duration).String()).Int64("rows", values[5].(int64)).Msg("SQL: " + sql) + } else if level == "log" { + if len(values) > 2 { + if err, ok := values[2].(error); ok { + log.Error().Err(err).Msg("Database error") + return + } + } + log.Debug().Msg(fmt.Sprint(values[2:]...)) + } else { + log.Error().Msgf("DatabaseLogger: cannot handle level %#v", level) + } + } + // ignore empty log +} diff --git a/logger/database_test.go b/logger/database_test.go new file mode 100644 index 0000000..198619e --- /dev/null +++ b/logger/database_test.go @@ -0,0 +1,96 @@ +package logger_test + +import ( + "testing" + "time" + + "github.com/rs/zerolog" + "github.com/traggo/server/logger" + "github.com/traggo/server/test" +) + +type Test struct { + Password []byte + NullValue *string + Placeholder string + Date time.Time + Number uint +} + +func TestDatabaseLogger_Success(t *testing.T) { + someTime, _ := time.Parse("2006/01/02", "2017/01/02") + db := test.InMemoryDB(t) + defer db.Close() + db.AutoMigrate(&Test{}) + + fakeLog := test.NewLogger(t) + defer fakeLog.Dispose() + + db.Create(&Test{ + Password: []byte{1}, + Date: someTime, + NullValue: nil, + Number: 1, + Placeholder: "hello there", + }) + + expected := `SQL: INSERT INTO "tests" ("password","null_value","placeholder","date","number") VALUES ('',NULL,'hello there','2017-01-02 00:00:00','1')` + + fakeLog.AssertCount(1) + fakeLog.AssertEntryExists(test.Entry{Level: zerolog.DebugLevel, Message: expected}) +} + +func TestDatabaseLogger_Error(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.AutoMigrate(&Test{}) + + fakeLog := test.NewLogger(t) + defer fakeLog.Dispose() + + // column id doesn't exists + db.Model(new(Test)).Where("id = ?", 5).Update("number", 6) + + expectedOne := `SQL: UPDATE "tests" SET "number" = '6' WHERE (id = '5')` + expectedTwo := `Database error` + + fakeLog.AssertCount(2) + fakeLog.AssertEntryExists(test.Entry{Level: zerolog.DebugLevel, Message: expectedOne}) + fakeLog.AssertEntryExists(test.Entry{Level: zerolog.ErrorLevel, Message: expectedTwo}) +} + +func TestDatabaseLogger_EmptyLog(t *testing.T) { + fakeLog := test.NewLogger(t) + defer fakeLog.Dispose() + + databaseLogger := logger.DatabaseLogger{} + databaseLogger.Print([]interface{}{}) + + fakeLog.AssertCount(0) +} + +func TestDatabaseLogger_UnkownType(t *testing.T) { + fakeLog := test.NewLogger(t) + defer fakeLog.Dispose() + + databaseLogger := logger.DatabaseLogger{} + databaseLogger.Print("abc", "somevalue") + + expected := `DatabaseLogger: cannot handle level "abc"` + + fakeLog.AssertCount(1) + fakeLog.AssertEntryExists(test.Entry{Level: zerolog.ErrorLevel, Message: expected}) +} + +func TestDatabaseLogger_LogWithoutError(t *testing.T) { + fakeLog := test.NewLogger(t) + defer fakeLog.Dispose() + + databaseLogger := logger.DatabaseLogger{} + databaseLogger.Print("log", "somefile", "my log message") + + expected := `my log message` + + fakeLog.AssertCount(1) + fakeLog.AssertEntryExists(test.Entry{Level: zerolog.DebugLevel, Message: expected}) +} diff --git a/logger/gql.go b/logger/gql.go new file mode 100644 index 0000000..0172ebe --- /dev/null +++ b/logger/gql.go @@ -0,0 +1,50 @@ +package logger + +import ( + "context" + "regexp" + "strings" + "time" + "unicode" + + "github.com/99designs/gqlgen/graphql" + "github.com/rs/zerolog/log" +) + +var ( + passRegEx = regexp.MustCompile(`pass\s*:\s*"(?:[^"\\]|\\.)*"`) +) + +// GQLLog logs graphql queries, mutations and errors. +func GQLLog() graphql.ResponseMiddleware { + return func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response { + start := time.Now() + result := next(ctx) + elapsed := time.Now().Sub(start) + errs := graphql.GetErrors(ctx) + rawQuery := graphql.GetRequestContext(ctx).RawQuery + + if len(errs) > 0 { + var errorStrings []string + for _, err := range errs { + errorStrings = append(errorStrings, err.Error()) + } + + log.Error().Strs("error", errorStrings).Str("took", elapsed.String()).Msg("GQL: " + toOneLine(hidePassword(rawQuery))) + } else if log.Debug().Enabled() { + log.Debug().Str("took", elapsed.String()).Msg("GQL: " + toOneLine(hidePassword(rawQuery))) + } + + return result + } +} + +func toOneLine(s string) string { + return strings.Join(strings.FieldsFunc(s, func(r rune) bool { + return unicode.IsSpace(r) || r == '\n' || r == '\r' + }), " ") +} + +func hidePassword(s string) string { + return passRegEx.ReplaceAllString(s, `pass:""`) +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..0674b13 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,15 @@ +package logger + +import ( + "os" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// Init initializes the logger +func Init(lvl zerolog.Level) { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}).Level(lvl) + log.Debug().Msg("Logger initialized") +} diff --git a/logger/logger_test.go b/logger/logger_test.go new file mode 100644 index 0000000..48856e4 --- /dev/null +++ b/logger/logger_test.go @@ -0,0 +1,14 @@ +package logger_test + +import ( + "testing" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/traggo/server/logger" +) + +func TestInit_LoggingWorks(t *testing.T) { + logger.Init(zerolog.InfoLevel) + log.Info().Msg("test logging") +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3a63a30 --- /dev/null +++ b/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "os" + "time" + + "github.com/gorilla/mux" + "github.com/jinzhu/gorm" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/traggo/server/auth" + "github.com/traggo/server/config" + "github.com/traggo/server/config/mode" + "github.com/traggo/server/database" + "github.com/traggo/server/graphql" + "github.com/traggo/server/logger" + "github.com/traggo/server/model" + "github.com/traggo/server/server" + "github.com/traggo/server/ui" + "github.com/traggo/server/user/password" +) + +var ( + // BuildMode will be set via ldflags + BuildMode = mode.Dev + // BuildCommit will be set via ldflags + BuildCommit = "-" + // BuildVersion will be set via ldflags + BuildVersion = "develop" + // BuildDate will be set via ldflags + BuildDate = "unknown" +) + +func main() { + mode.Set(BuildMode) + + version := model.Version{Commit: BuildCommit, BuildDate: BuildDate, Name: BuildVersion} + + conf, errs := config.Get() + logger.Init(conf.LogLevel.AsZeroLogLevel()) + log.Info().Str("commit", BuildCommit).Str("buildDate", BuildDate).Str("buildMode", BuildMode).Str("version", BuildVersion).Msg("Traggo") + + exit := false + for _, err := range errs { + log.WithLevel(err.Level).Msg(err.Msg) + exit = exit || err.Level == zerolog.FatalLevel || err.Level == zerolog.PanicLevel + } + if exit { + os.Exit(1) + } + log.Debug().Interface("config", conf).Msg("Using") + + db := initDatabase(conf) + + stopCleanUp := initCleanUp(db) + + router := initRouter(db, conf, version) + + log.Info().Int("port", conf.Port).Msg("Start listening") + if err := server.Start(router, conf.Port); err != nil { + log.Fatal().Err(err).Msg("Server Error") + } + stopCleanUp <- true +} + +func initRouter(db *gorm.DB, conf config.Config, version model.Version) *mux.Router { + gqlHandler := graphql.Handler( + "/graphql", + graphql.NewResolver(db, conf.PassStrength, version), + graphql.NewDirective()) + + router := mux.NewRouter() + router.Use(auth.Middleware(db)) + router.HandleFunc("/graphql", gqlHandler) + ui.Register(router) + return router +} + +func initDatabase(conf config.Config) *gorm.DB { + db, err := database.New(conf.DatabaseDialect, conf.DatabaseConnection) + if err != nil { + log.Fatal().Err(err).Msg("Failed to connect to the database") + } + c := new(int) + if db.Model(new(model.User)).Count(c); *c == 0 { + log.Info().Msg("Creating default user.") + db.Create(&model.User{ + Name: conf.DefaultUserName, + Pass: password.CreatePassword(conf.DefaultUserPass, conf.PassStrength), + Admin: true}) + } + + return db +} + +func initCleanUp(db *gorm.DB) chan<- bool { + stopCleanUp := make(chan bool) + go auth.CleanUp(db, time.Minute*10, stopCleanUp) + return stopCleanUp +} diff --git a/model/all.go b/model/all.go new file mode 100644 index 0000000..7f71a3e --- /dev/null +++ b/model/all.go @@ -0,0 +1,17 @@ +package model + +// All returns all schema instances. +func All() []interface{} { + return []interface{}{ + new(TagDefinition), + new(User), + new(Device), + new(TimeSpan), + new(TimeSpanTag), + new(UserSetting), + new(Dashboard), + new(DashboardEntry), + new(DashboardTagFilter), + new(DashboardRange), + } +} diff --git a/model/all_test.go b/model/all_test.go new file mode 100644 index 0000000..8ab6962 --- /dev/null +++ b/model/all_test.go @@ -0,0 +1,17 @@ +package model_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/traggo/server/model" +) + +func TestAll_NoDuplicateEntries(t *testing.T) { + all := model.All() + var checkedItems []interface{} + for _, item := range all { + assert.NotContains(t, checkedItems, item) + checkedItems = append(checkedItems, item) + } +} diff --git a/model/dashboard.go b/model/dashboard.go new file mode 100644 index 0000000..4e9190a --- /dev/null +++ b/model/dashboard.go @@ -0,0 +1,106 @@ +package model + +import ( + "database/sql/driver" + "fmt" +) + +// DashboardRange a named range of a dashboard. +type DashboardRange struct { + ID int `gorm:"primary_key;unique_index;AUTO_INCREMENT"` + Name string + DashboardID int `gorm:"type:int REFERENCES dashboards(id) ON DELETE CASCADE"` + Editable bool + From string + To string +} + +// Dashboard a dashboard +type Dashboard struct { + ID int `gorm:"primary_key;unique_index;AUTO_INCREMENT"` + UserID int `gorm:"type:int REFERENCES users(id) ON DELETE CASCADE"` + Name string + Entries []DashboardEntry + Ranges []DashboardRange +} + +// DashboardEntry an entry which represents a diagram in a dashboard. +type DashboardEntry struct { + ID int `gorm:"primary_key;unique_index;AUTO_INCREMENT"` + DashboardID int `gorm:"type:int REFERENCES dashboards(id) ON DELETE CASCADE"` + Title string + Total bool `gorm:"default:false"` + Type DashboardType + Keys string + Interval Interval + RangeID int + RangeFrom string + RangeTo string + TagFilters []DashboardTagFilter + + MobilePosition string + DesktopPosition string +} + +// DashboardTagFilter a tag for filtering timespans +type DashboardTagFilter struct { + DashboardEntryID int `gorm:"type:int REFERENCES dashboard_entries(id) ON DELETE CASCADE"` + Key string + StringValue string + Include bool +} + +// DashboardType the dashboard type +type DashboardType string + +// Value for db +func (t DashboardType) Value() (driver.Value, error) { + return string(t), nil +} + +// Scan for db +func (t *DashboardType) Scan(value interface{}) error { + s, ok := value.([]byte) + if !ok { + return fmt.Errorf("expected string but was %#v", value) + } + *t = DashboardType(s) + return nil +} + +// Interval the interval in which the diagram data should be grouped. +type Interval string + +// Value for db +func (t Interval) Value() (driver.Value, error) { + return string(t), nil +} + +// Scan for db +func (t *Interval) Scan(value interface{}) error { + s, ok := value.([]byte) + if !ok { + return fmt.Errorf("expected string but was %#v", value) + } + *t = Interval(s) + return nil +} + +// No lint errors please. +const ( + TypePieChart DashboardType = "piechart" + TypeBarChart DashboardType = "barchart" + TypeLineChart DashboardType = "linechart" + HorizontalTable DashboardType = "horizontaltable" + VerticalTable DashboardType = "verticaltable" + TypeStackedBarChart DashboardType = "stackedbarchart" + + IntervalHourly Interval = "hourly" + IntervalDaily Interval = "daily" + IntervalWeekly Interval = "weekly" + IntervalMonthly Interval = "monthly" + IntervalYearly Interval = "yearly" + IntervalSingle Interval = "single" + + NoRangeIDDefined = -1 +) diff --git a/model/device.go b/model/device.go new file mode 100644 index 0000000..589ca63 --- /dev/null +++ b/model/device.go @@ -0,0 +1,68 @@ +package model + +import ( + "database/sql/driver" + "errors" + "fmt" + "time" +) + +// Device represents something which can connect to traggo +type Device struct { + ID int `gorm:"primary_key;unique_index;AUTO_INCREMENT"` + Token string `gorm:"unique"` + Name string + UserID int `gorm:"type:int REFERENCES users(id) ON DELETE CASCADE"` + CreatedAt time.Time + Type DeviceType + ActiveAt time.Time +} + +// DeviceType the device type +type DeviceType string + +// Value for db +func (t DeviceType) Value() (driver.Value, error) { + return string(t), nil +} + +// Scan for db +func (t *DeviceType) Scan(value interface{}) error { + s, ok := value.([]byte) + if !ok { + return fmt.Errorf("expected []byte but was %#v", value) + } + *t = DeviceType(s) + return nil +} + +// Seconds returns the amount of seconds after the device expires. +func (t DeviceType) Seconds() int { + switch t { + case TypeLongExpiry: + return 60 * 60 * 24 * 30 + case TypeShortExpiry: + return 60 * 60 + case TypeNoExpiry: + return 60 * 60 * 24 * 365 + default: + return 0 + } +} + +// Valid checks if the device type is valid. +func (t DeviceType) Valid() error { + switch t { + case TypeNoExpiry, TypeShortExpiry, TypeLongExpiry: + return nil + default: + return errors.New("unknown device type") + } +} + +// Device types +const ( + TypeShortExpiry DeviceType = "ShortExpiry" + TypeLongExpiry DeviceType = "LongExpiry" + TypeNoExpiry DeviceType = "NoExpiry" +) diff --git a/model/device_test.go b/model/device_test.go new file mode 100644 index 0000000..cc0fcc9 --- /dev/null +++ b/model/device_test.go @@ -0,0 +1,26 @@ +package model + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test(t *testing.T) { + dType := DeviceType("abc") + require.Error(t, dType.Valid(), "unknown type") + + require.Equal(t, TypeShortExpiry.Seconds(), 3600) + require.Equal(t, TypeLongExpiry.Seconds(), 2592000) + require.Equal(t, TypeNoExpiry.Seconds(), 31536000) + value, err := TypeNoExpiry.Value() + require.NoError(t, err) + require.Equal(t, value, "NoExpiry") + require.Nil(t, TypeNoExpiry.Valid()) + + var scan DeviceType + + err = scan.Scan([]byte("NoExpiry")) + require.NoError(t, err) + require.Equal(t, TypeNoExpiry, scan) +} diff --git a/model/setting.go b/model/setting.go new file mode 100644 index 0000000..8356ff6 --- /dev/null +++ b/model/setting.go @@ -0,0 +1,49 @@ +package model + +import "time" + +// UserSetting a setting for a user. +type UserSetting struct { + UserID int `gorm:"primary_key;unique_index"` + Theme string + DateLocale string + FirstDayOfTheWeek string + DateTimeInputStyle string +} + +// Settings constants +const ( + ThemeGruvboxDark = "GruvboxDark" + ThemeGruvboxLight = "GruvboxLight" + ThemeMaterialDark = "MaterialDark" + ThemeMaterialLight = "MaterialLight" + + DateLocaleGerman = "German" + DateLocaleAmerican = "American" + DateLocaleAmerican24h = "American24h" + DateLocaleBritish = "British" + DateLocaleAustralian = "Australian" + + DateTimeInputFancy = "Fancy" + DateTimeInputNative = "Native" +) + +var daysOfWeek = map[string]time.Weekday{ + "Sunday": time.Sunday, + "Monday": time.Monday, + "Tuesday": time.Tuesday, + "Wednesday": time.Wednesday, + "Thursday": time.Thursday, + "Friday": time.Friday, + "Saturday": time.Saturday, +} + +// FirstDayOfTheWeekTimeWeekday returns the configured first day of the week. +func (s UserSetting) FirstDayOfTheWeekTimeWeekday() time.Weekday { + return daysOfWeek[s.FirstDayOfTheWeek] +} + +// LastDayOfTheWeekTimeWeekday returns the configured last day of the week. +func (s UserSetting) LastDayOfTheWeekTimeWeekday() time.Weekday { + return daysOfWeek[(daysOfWeek[s.FirstDayOfTheWeek] - 1).String()] +} diff --git a/model/tagdefinition.go b/model/tagdefinition.go new file mode 100644 index 0000000..d7be704 --- /dev/null +++ b/model/tagdefinition.go @@ -0,0 +1,9 @@ +package model + +// TagDefinition describes a tag. +type TagDefinition struct { + Key string + UserID int `gorm:"type:int REFERENCES users(id) ON DELETE CASCADE"` + Color string + Usages int `gorm:"-"` +} diff --git a/model/time.go b/model/time.go new file mode 100644 index 0000000..156ebf4 --- /dev/null +++ b/model/time.go @@ -0,0 +1,48 @@ +package model + +import ( + "fmt" + "io" + "time" +) + +// Time scalar type for graphql +type Time time.Time + +// Time returns the wrapped time +func (t Time) Time() time.Time { + return time.Time(t) +} + +// OmitTimeZone omits the time zone and removes a utc date. +func (t Time) OmitTimeZone() time.Time { + x := t.Time() + return time.Date(x.Year(), x.Month(), x.Day(), x.Hour(), x.Minute(), x.Second(), x.Nanosecond(), time.UTC) +} + +// UTC changes the timezone to utc. +func (t Time) UTC() time.Time { + return t.Time().UTC() +} + +// MarshalGQL implements the graphql.Marshaler interface +func (t Time) MarshalGQL(w io.Writer) { + if _, err := w.Write([]byte(fmt.Sprintf(`"%s"`, t.Time().Format(time.RFC3339)))); err != nil { + panic(err) + } +} + +// UnmarshalGQL implements the graphql.Unmarshaler interface +func (t *Time) UnmarshalGQL(v interface{}) error { + raw, ok := v.(string) + if !ok { + return fmt.Errorf("time must be a string") + } + + parse, err := time.Parse(time.RFC3339, raw) + if err != nil { + return err + } + *t = Time(parse) + return nil +} diff --git a/model/time_test.go b/model/time_test.go new file mode 100644 index 0000000..4317253 --- /dev/null +++ b/model/time_test.go @@ -0,0 +1,82 @@ +package model + +import ( + "bytes" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestTime_MarshalGQL(t *testing.T) { + var buffer bytes.Buffer + expected := `2009-06-30T18:30:00+02:00` + parse, err := time.Parse(time.RFC3339, expected) + assert.Nil(t, err) + toTest := Time(parse) + toTest.MarshalGQL(&buffer) + actual := buffer.String() + assert.Equal(t, `"`+expected+`"`, actual) +} + +func TestTime_MarshalGQL_fails(t *testing.T) { + toTest := &Time{} + assert.Panics(t, func() { + toTest.MarshalGQL(&failWriter{}) + }) +} + +type failWriter struct { +} + +func (*failWriter) Write(p []byte) (n int, err error) { + return 0, errors.New("uff") +} + +func TestTime_UnmarshalGQL_success(t *testing.T) { + date := "2009-06-30T18:30:00+02:00" + parse, err := time.Parse(time.RFC3339, date) + assert.Nil(t, err) + expected := Time(parse) + + actual := &Time{} + err = actual.UnmarshalGQL(date) + assert.Nil(t, err) + + assert.Equal(t, expected, *actual) +} + +func TestTime_OmitTimeZone(t *testing.T) { + date := "2009-06-30T18:30:00+02:00" + tzDate, err := time.Parse(time.RFC3339, date) + assert.Nil(t, err) + utcDate := "2009-06-30T18:30:00Z" + withoutTz, err := time.Parse(time.RFC3339, utcDate) + assert.Nil(t, err) + + assert.Equal(t, withoutTz, Time(tzDate).OmitTimeZone()) +} + +func TestTime_UTC(t *testing.T) { + date := "2009-06-30T18:30:00+02:00" + parse, err := time.Parse(time.RFC3339, date) + assert.Nil(t, err) + without := "2009-06-30T16:30:00Z" + withoutTz, err := time.Parse(time.RFC3339, without) + assert.Nil(t, err) + + assert.Equal(t, withoutTz, Time(parse).UTC()) +} + +func TestTime_UnmarshalGQL_failInvalidType(t *testing.T) { + actual := &Time{} + err := actual.UnmarshalGQL(1) + assert.EqualError(t, err, "time must be a string") +} + +func TestTime_UnmarshalGQL_invalidFormat(t *testing.T) { + actual := &Time{} + err := actual.UnmarshalGQL("lol") + assert.EqualError(t, err, `parsing time "lol" as "2006-01-02T15:04:05Z07:00": cannot parse "lol" as "2006"`) +} diff --git a/model/timespan.go b/model/timespan.go new file mode 100644 index 0000000..980ae10 --- /dev/null +++ b/model/timespan.go @@ -0,0 +1,23 @@ +package model + +import "time" + +// TimeSpan is basically a tagged time range. +type TimeSpan struct { + ID int `gorm:"primary_key;unique_index;AUTO_INCREMENT"` + StartUTC time.Time + EndUTC *time.Time + StartUserTime time.Time + EndUserTime *time.Time + OffsetUTC int + UserID int `gorm:"type:int REFERENCES users(id) ON DELETE CASCADE"` + Tags []TimeSpanTag + Note string +} + +// TimeSpanTag is a tag for a time range +type TimeSpanTag struct { + TimeSpanID int `gorm:"type:int REFERENCES time_spans(id) ON DELETE CASCADE"` + Key string + StringValue string +} diff --git a/model/user.go b/model/user.go new file mode 100644 index 0000000..1306e9f --- /dev/null +++ b/model/user.go @@ -0,0 +1,9 @@ +package model + +// User holds information about credentials and authorizations. +type User struct { + ID int `gorm:"primary_key;unique_index;AUTO_INCREMENT"` + Name string `gorm:"unique"` + Pass []byte + Admin bool +} diff --git a/model/version.go b/model/version.go new file mode 100644 index 0000000..d005689 --- /dev/null +++ b/model/version.go @@ -0,0 +1,8 @@ +package model + +// The Version of traggo. +type Version struct { + Commit string + Name string + BuildDate string +} diff --git a/schema.graphql b/schema.graphql new file mode 100644 index 0000000..4efef71 --- /dev/null +++ b/schema.graphql @@ -0,0 +1,299 @@ +enum Role { + ADMIN + USER +} +directive @hasRole(role: Role!) on FIELD_DEFINITION + +schema { + query: RootQuery + mutation: RootMutation +} + +type RootMutation { + createTag(key: String!, color: String!): TagDefinition @hasRole(role: USER) + updateTag(key: String!, newKey: String, color: String!): TagDefinition @hasRole(role: USER) + removeTag(key: String!): TagDefinition @hasRole(role: USER) + + createUser(name: String!, pass: String!, admin: Boolean!): User @hasRole(role: ADMIN) + removeUser(id: Int!): User @hasRole(role: ADMIN) + updateUser(id: Int!, name: String!, pass: String, admin: Boolean!): User @hasRole(role: ADMIN) + + login(username: String!, pass: String!, deviceName: String!, type: DeviceType!, cookie: Boolean!): Login + + createDevice(name: String!, type: DeviceType!): Login @hasRole(role: USER) + updateDevice(id: Int!, name: String!, type: DeviceType!): Device @hasRole(role: USER) + removeDevice(id: Int!): Device @hasRole(role: USER) + removeCurrentDevice: Device! @hasRole(role: USER) + + createTimeSpan(start: Time!, end: Time, tags: [InputTimeSpanTag!], note: String!): TimeSpan @hasRole(role: USER) + updateTimeSpan(id: Int!, start: Time!, end: Time, tags: [InputTimeSpanTag!], oldStart: Time, note: String!): TimeSpan @hasRole(role: USER) + removeTimeSpan(id: Int!): TimeSpan @hasRole(role: USER) + replaceTimeSpanTags(from: InputTimeSpanTag!, to: InputTimeSpanTag!, opt: InputReplaceOptions!): Boolean @hasRole(role: USER) + copyTimeSpan(id: Int!, start: Time!, end: Time): TimeSpan @hasRole(role: USER) + stopTimeSpan(id: Int!, end: Time!): TimeSpan @hasRole(role: USER) + + createDashboard(name: String!): Dashboard! @hasRole(role: USER) + updateDashboard(id: Int!, name: String!): Dashboard! @hasRole(role: USER) + removeDashboard(id: Int!): Dashboard! @hasRole(role: USER) + + addDashboardRange(dashboardId: Int!, range: InputNamedDateRange!): NamedDateRange @hasRole(role: USER) + updateDashboardRange(rangeId: Int!, range: InputNamedDateRange!): NamedDateRange @hasRole(role: USER) + removeDashboardRange(rangeId: Int!): NamedDateRange @hasRole(role: USER) + + addDashboardEntry(dashboardId: Int!, entryType: EntryType!, title: String!, total: Boolean!, stats: InputStatsSelection!, pos: InputResponsiveDashboardEntryPos): DashboardEntry @hasRole(role: USER) + updateDashboardEntry(entryId: Int!, entryType: EntryType, title: String, total: Boolean, stats: InputStatsSelection, pos: InputResponsiveDashboardEntryPos): DashboardEntry @hasRole(role: USER) + removeDashboardEntry(id: Int!): DashboardEntry! @hasRole(role: USER) + + setUserSettings(settings: InputUserSettings!): UserSettings! @hasRole(role: USER) +} + +type RootQuery { + suggestTag(query: String!): [TagDefinition!] @hasRole(role: USER) + suggestTagValue(key: String!, query: String!): [String!] @hasRole(role: USER) + tags: [TagDefinition!] @hasRole(role: USER) + users: [User!] @hasRole(role: ADMIN) + currentUser: User + currentDevice: Device + devices: [Device!] @hasRole(role: USER) + timeSpans(fromInclusive: Time, toInclusive: Time, cursor: InputCursor): PagedTimeSpans! @hasRole(role: USER) + timers: [TimeSpan!] @hasRole(role: USER) + stats(ranges: [Range!], tags: [String!], excludeTags: [InputTimeSpanTag!], requireTags: [InputTimeSpanTag!]): [RangedStatisticsEntries!] @hasRole(role: USER) + stats2(now: Time!, stats: InputStatsSelection!): [RangedStatisticsEntries!] @hasRole(role: USER) + version: Version! + dashboards: [Dashboard!] @hasRole(role: USER) + userSettings: UserSettings! +} + +input InputUserSettings { + theme: Theme! + dateLocale: DateLocale! + firstDayOfTheWeek: WeekDay! + dateTimeInputStyle: DateTimeInputStyle! +} + +type UserSettings { + theme: Theme! + dateLocale: DateLocale! + firstDayOfTheWeek: WeekDay! + dateTimeInputStyle: DateTimeInputStyle! +} + +enum WeekDay { + Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday +} + +enum Theme { + GruvboxDark, GruvboxLight, MaterialDark, MaterialLight +} + +enum DateLocale { + American, American24h, German, Australian, British +} + +enum DateTimeInputStyle { + Fancy, Native +} + +input InputReplaceOptions { + override: OverrideMode! +} + +enum OverrideMode { + Override, Discard +} + +input InputCursor { + offset: Int + startId: Int + pageSize: Int +} + +type Cursor { + hasMore: Boolean! + offset: Int! + startId: Int! + pageSize: Int! +} + +type Version { + name: String! + commit: String! + buildDate: String! +} + +type Login { + token: String! + user: User! + device: Device! +} + +type Device { + id: Int! + name: String! + type: DeviceType! + createdAt: Time! + activeAt: Time! +} + +enum DeviceType { + LongExpiry, ShortExpiry, NoExpiry +} + +scalar Time + +type TagDefinition { + color: String! + key: String! + user: User! + usages: Int! +} + +type User { + admin: Boolean! + id: Int! + name: String! +} + +type TimeSpan { + id: Int! + start: Time! + end: Time + oldStart: Time + tags: [TimeSpanTag!] + # TODO not nullable ^ + note: String! +} + +type PagedTimeSpans { + timeSpans: [TimeSpan!]! + cursor: Cursor! +} + +type TimeSpanTag { + key: String! + value: String! +} + +input InputTimeSpanTag { + key: String! + value: String! +} + +input StatInput { + key: String! + mustHave: [InputTimeSpanTag!] + mustNotHave: [InputTimeSpanTag!] +} + +input Range { + start: Time! + end: Time! +} + +type StatisticsEntry { + key: String! + value: String! + timeSpendInSeconds: Float! +} + +type RangedStatisticsEntries { + start: Time! + end: Time! + entries: [StatisticsEntry!] +} + +type Dashboard { + id: Int! + name: String!, + items: [DashboardEntry!]! + ranges: [NamedDateRange!]! +} + +type NamedDateRange { + id: Int! + name: String! + editable: Boolean! + range: RelativeOrStaticRange! +} + +input InputNamedDateRange { + name: String! + editable: Boolean! + range: InputRelativeOrStaticRange! +} + +type StatsSelection { + interval: StatsInterval! + tags: [String!] + excludeTags: [TimeSpanTag!] + includeTags: [TimeSpanTag!] + rangeId: Int + range: RelativeOrStaticRange +} + +input InputStatsSelection { + interval: StatsInterval! + tags: [String!] + excludeTags: [InputTimeSpanTag!] + includeTags: [InputTimeSpanTag!] + rangeId: Int + range: InputRelativeOrStaticRange +} + +enum StatsInterval { + Hourly, Daily, Weekly, Monthly, Yearly, Single +} + +type RelativeOrStaticRange { + from: String! + to: String! +} + +input InputRelativeOrStaticRange { + from: String! + to: String! +} + +type DashboardEntry { + id: Int! + title: String! + total: Boolean! + statsSelection: StatsSelection! + pos: ResponsiveDashboardEntryPos! + entryType: EntryType! +} + +type ResponsiveDashboardEntryPos { + desktop: DashboardEntryPos! + mobile: DashboardEntryPos! +} +input InputResponsiveDashboardEntryPos { + desktop: InputDashboardEntryPos + mobile: InputDashboardEntryPos +} + +input InputDashboardEntryPos { + w: Int! + h: Int! + x: Int! + y: Int! +} + +type DashboardEntryPos { + w: Int! + h: Int! + x: Int! + y: Int! + minW: Int! + minH: Int! +} + +enum DashboardSize { + Large, + Medium, + Small +} + +enum EntryType { + PieChart, BarChart, StackedBarChart, LineChart, VerticalTable, HorizontalTable +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..a9a39d4 --- /dev/null +++ b/server/server.go @@ -0,0 +1,62 @@ +package server + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "time" + + "github.com/gorilla/mux" +) + +var notifySignal = signal.Notify +var serverShutdown = func(server *http.Server, ctx context.Context) error { + return server.Shutdown(ctx) +} + +// Start starts the http server +func Start(mux *mux.Router, port int) error { + server, shutdown := startServer(mux, port) + shutdownOnInterruptSignal(server, 2*time.Second, shutdown) + return waitForServerToClose(shutdown) +} + +func startServer(mux *mux.Router, port int) (*http.Server, chan error) { + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: mux, + } + + shutdown := make(chan error) + go func() { + err := srv.ListenAndServe() + shutdown <- err + }() + return srv, shutdown +} + +func shutdownOnInterruptSignal(server *http.Server, timeout time.Duration, shutdown chan<- error) { + interrupt := make(chan os.Signal, 1) + notifySignal(interrupt, os.Interrupt) + + go func() { + select { + case <-interrupt: + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + if err := serverShutdown(server, ctx); err != nil { + shutdown <- err + } + } + }() +} + +func waitForServerToClose(shutdown <-chan error) error { + err := <-shutdown + if err == http.ErrServerClosed { + return nil + } + return err +} diff --git a/server/server_test.go b/server/server_test.go new file mode 100644 index 0000000..29ef348 --- /dev/null +++ b/server/server_test.go @@ -0,0 +1,105 @@ +package server + +import ( + "context" + "errors" + "net/http" + "os" + "testing" + "time" + + "github.com/gorilla/mux" + "github.com/phayes/freeport" + "github.com/stretchr/testify/assert" + "github.com/traggo/server/test" +) + +func TestShutdownOnErrorWhileShutdown(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + disposeInterrupt := fakeInterrupt(t) + defer disposeInterrupt() + + shutdownError := errors.New("shutdown error") + disposeShutdown := fakeShutdownError(shutdownError) + defer disposeShutdown() + + finished := make(chan error) + + go func() { + finished <- Start(mux.NewRouter(), freeport.GetPort()) + }() + + select { + case <-time.After(1 * time.Second): + t.Fatal("Server should be closed") + case err := <-finished: + assert.Equal(t, shutdownError, err) + } +} + +func TestShutdownAfterError(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + finished := make(chan error) + + go func() { + finished <- Start(mux.NewRouter(), -5) + }() + + select { + case <-time.After(1 * time.Second): + t.Fatal("Server should be closed") + case err := <-finished: + assert.NotNil(t, err) + } +} + +func TestShutdown(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + dispose := fakeInterrupt(t) + defer dispose() + + finished := make(chan error) + + go func() { + finished <- Start(mux.NewRouter(), freeport.GetPort()) + }() + + select { + case <-time.After(1 * time.Second): + t.Fatal("Server should be closed") + case err := <-finished: + assert.Nil(t, err) + } +} + +func fakeInterrupt(t *testing.T) func() { + oldNotify := notifySignal + notifySignal = func(c chan<- os.Signal, sig ...os.Signal) { + assert.Contains(t, sig, os.Interrupt) + go func() { + select { + case <-time.After(100 * time.Millisecond): + c <- os.Interrupt + } + }() + } + return func() { + notifySignal = oldNotify + } +} + +func fakeShutdownError(err error) func() { + old := serverShutdown + serverShutdown = func(server *http.Server, ctx context.Context) error { + return err + } + return func() { + serverShutdown = old + } +} diff --git a/setting/get.go b/setting/get.go new file mode 100644 index 0000000..a6cace3 --- /dev/null +++ b/setting/get.go @@ -0,0 +1,37 @@ +package setting + +import ( + "context" + "time" + + "github.com/jinzhu/gorm" + "github.com/traggo/server/auth" + "github.com/traggo/server/model" +) + +// Get returns the settings +func Get(ctx context.Context, db *gorm.DB) (model.UserSetting, error) { + internal := model.UserSetting{} + user := auth.GetUser(ctx) + defaultSettings := model.UserSetting{ + Theme: model.ThemeGruvboxDark, + DateLocale: model.DateLocaleAmerican, + FirstDayOfTheWeek: time.Monday.String(), + DateTimeInputStyle: model.DateTimeInputFancy, + } + + if user == nil { + return defaultSettings, nil + } + find := db.Where(&model.UserSetting{UserID: user.ID}).Find(&internal) + + if find.RecordNotFound() { + return defaultSettings, nil + } + + if find.Error != nil { + return defaultSettings, find.Error + } + + return internal, nil +} diff --git a/setting/get_test.go b/setting/get_test.go new file mode 100644 index 0000000..ef86b58 --- /dev/null +++ b/setting/get_test.go @@ -0,0 +1,47 @@ +package setting + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestGet_noUser(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + settings, err := Get(context.Background(), db.DB) + require.NoError(t, err) + require.Equal(t, model.ThemeGruvboxDark, settings.Theme) +} + +func TestGet_user_noSettings(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(1) + + settings, err := Get(fake.User(1), db.DB) + require.NoError(t, err) + require.Equal(t, model.ThemeGruvboxDark, settings.Theme) +} + +func TestGet_user(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(1) + db.Save(&model.UserSetting{ + UserID: 1, + Theme: model.ThemeGruvboxLight, + DateLocale: model.DateLocaleGerman, + FirstDayOfTheWeek: time.Sunday.String(), + }) + + settings, err := Get(fake.User(1), db.DB) + require.NoError(t, err) + require.Equal(t, model.ThemeGruvboxLight, settings.Theme) +} diff --git a/setting/settingresolver.go b/setting/settingresolver.go new file mode 100644 index 0000000..0017dfe --- /dev/null +++ b/setting/settingresolver.go @@ -0,0 +1,8 @@ +package setting + +import "github.com/jinzhu/gorm" + +// ResolverForSettings resolves setting specific things. +type ResolverForSettings struct { + DB *gorm.DB +} diff --git a/setting/usersettings.go b/setting/usersettings.go new file mode 100644 index 0000000..a305374 --- /dev/null +++ b/setting/usersettings.go @@ -0,0 +1,139 @@ +package setting + +import ( + "context" + "time" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// SetUserSettings sets the user settings. +func (r *ResolverForSettings) SetUserSettings(ctx context.Context, settings gqlmodel.InputUserSettings) (*gqlmodel.UserSettings, error) { + internal := model.UserSetting{ + Theme: toInternalTheme(settings.Theme), + FirstDayOfTheWeek: toInternalWeekday(settings.FirstDayOfTheWeek).String(), + UserID: auth.GetUser(ctx).ID, + DateLocale: toInternalDateLocale(settings.DateLocale), + DateTimeInputStyle: toInternalDateTimeInputStyle(settings.DateTimeInputStyle), + } + + save := r.DB.Save(internal) + + return toExternal(internal), save.Error +} + +// UserSettings returns the user settings. +func (r *ResolverForSettings) UserSettings(ctx context.Context) (*gqlmodel.UserSettings, error) { + settings, err := Get(ctx, r.DB) + return toExternal(settings), err +} + +func toExternal(internal model.UserSetting) *gqlmodel.UserSettings { + return &gqlmodel.UserSettings{ + Theme: toExternalTheme(internal.Theme), + DateLocale: toExternalDateLocale(internal.DateLocale), + FirstDayOfTheWeek: toExternalWeekday(internal.FirstDayOfTheWeekTimeWeekday()), + DateTimeInputStyle: toExternalDateTimeInputStyle(internal.DateTimeInputStyle), + } +} + +func toInternalDateLocale(locale gqlmodel.DateLocale) string { + switch locale.String() { + case model.DateLocaleAmerican, model.DateLocaleGerman, model.DateLocaleAmerican24h, model.DateLocaleAustralian, model.DateLocaleBritish: + return locale.String() + default: + return model.DateLocaleAmerican + } +} + +func toExternalDateLocale(dateLocale string) gqlmodel.DateLocale { + if gqlmodel.DateLocale(dateLocale).IsValid() { + return gqlmodel.DateLocale(dateLocale) + } + if dateLocale == "English24h" { + return gqlmodel.DateLocaleAmerican24h + } + return gqlmodel.DateLocaleAmerican +} + +func toInternalTheme(theme gqlmodel.Theme) string { + switch theme.String() { + case model.ThemeGruvboxDark, model.ThemeGruvboxLight, model.ThemeMaterialLight, model.ThemeMaterialDark: + return theme.String() + default: + return model.ThemeGruvboxDark + } +} + +func toExternalTheme(theme string) gqlmodel.Theme { + if gqlmodel.Theme(theme).IsValid() { + return gqlmodel.Theme(theme) + } + return gqlmodel.ThemeGruvboxDark +} + +func toExternalWeekday(weekday time.Weekday) gqlmodel.WeekDay { + switch weekday { + case time.Monday: + return gqlmodel.WeekDayMonday + case time.Tuesday: + return gqlmodel.WeekDayTuesday + case time.Wednesday: + return gqlmodel.WeekDayWednesday + case time.Thursday: + return gqlmodel.WeekDayThursday + case time.Friday: + return gqlmodel.WeekDayFriday + case time.Saturday: + return gqlmodel.WeekDaySaturday + case time.Sunday: + return gqlmodel.WeekDaySunday + default: + panic("unknown weekday") + } +} + +func toInternalWeekday(weekday gqlmodel.WeekDay) time.Weekday { + switch weekday { + case gqlmodel.WeekDayMonday: + return time.Monday + case gqlmodel.WeekDayTuesday: + return time.Tuesday + case gqlmodel.WeekDayWednesday: + return time.Wednesday + case gqlmodel.WeekDayThursday: + return time.Thursday + case gqlmodel.WeekDayFriday: + return time.Friday + case gqlmodel.WeekDaySaturday: + return time.Saturday + case gqlmodel.WeekDaySunday: + return time.Sunday + default: + panic("unknown weekday") + } +} + +func toExternalDateTimeInputStyle(style string) gqlmodel.DateTimeInputStyle { + switch style { + case model.DateTimeInputFancy: + return gqlmodel.DateTimeInputStyleFancy + case model.DateTimeInputNative: + return gqlmodel.DateTimeInputStyleNative + default: + return gqlmodel.DateTimeInputStyleFancy + } +} + +func toInternalDateTimeInputStyle(style gqlmodel.DateTimeInputStyle) string { + switch style { + case gqlmodel.DateTimeInputStyleFancy: + return model.DateTimeInputFancy + case gqlmodel.DateTimeInputStyleNative: + return model.DateTimeInputNative + default: + return model.DateTimeInputFancy + } +} diff --git a/setting/usersettings_test.go b/setting/usersettings_test.go new file mode 100644 index 0000000..6d477bf --- /dev/null +++ b/setting/usersettings_test.go @@ -0,0 +1,68 @@ +package setting + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +const ( + invalidInternalWeekday = time.Weekday(100) + invalidExternalWeekday = gqlmodel.WeekDay("abc") +) + +func TestSettingsResolver(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + db.User(1) + + resolver := &ResolverForSettings{DB: db.DB} + + settings, err := resolver.UserSettings(fake.User(1)) + require.NoError(t, err) + require.Equal(t, gqlmodel.ThemeGruvboxDark, settings.Theme) + + _, err = resolver.SetUserSettings(fake.User(1), gqlmodel.InputUserSettings{ + Theme: gqlmodel.ThemeGruvboxLight, + DateLocale: gqlmodel.DateLocaleGerman, + FirstDayOfTheWeek: gqlmodel.WeekDayWednesday, + DateTimeInputStyle: gqlmodel.DateTimeInputStyleFancy, + }) + require.NoError(t, err) + + settings, err = resolver.UserSettings(fake.User(1)) + require.NoError(t, err) + require.Equal(t, &gqlmodel.UserSettings{ + Theme: gqlmodel.ThemeGruvboxLight, + DateLocale: gqlmodel.DateLocaleGerman, + FirstDayOfTheWeek: gqlmodel.WeekDayWednesday, + DateTimeInputStyle: gqlmodel.DateTimeInputStyleFancy, + }, settings) +} + +func TestWeekDayConvert(t *testing.T) { + for _, day := range gqlmodel.AllWeekDay { + require.Equal(t, day, toExternalWeekday(toInternalWeekday(day))) + } + require.Panics(t, func() { + toInternalWeekday(invalidExternalWeekday) + }) + require.Panics(t, func() { + toExternalWeekday(invalidInternalWeekday) + }) +} + +func TestShouldHandleInvalidInputs(t *testing.T) { + toExternalTheme("aoeuaoeu") + toInternalTheme("aoeuaoeu") + toExternalDateLocale("aeu") + toInternalDateLocale("aoeu") + toExternalDateTimeInputStyle("aoeu") + toInternalDateTimeInputStyle("aoeu") + +} diff --git a/statistics/statisticsresolver.go b/statistics/statisticsresolver.go new file mode 100644 index 0000000..e0cfb67 --- /dev/null +++ b/statistics/statisticsresolver.go @@ -0,0 +1,8 @@ +package statistics + +import "github.com/jinzhu/gorm" + +// ResolverForStatistics resolves statistic things. +type ResolverForStatistics struct { + DB *gorm.DB +} diff --git a/statistics/summary.go b/statistics/summary.go new file mode 100644 index 0000000..0f5bb91 --- /dev/null +++ b/statistics/summary.go @@ -0,0 +1,189 @@ +package statistics + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + "time" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// Stats groups the time spans by tag key. +func (r *ResolverForStatistics) Stats(ctx context.Context, ranges []*gqlmodel.Range, tags []string, excludeTags []*gqlmodel.InputTimeSpanTag, requireTags []*gqlmodel.InputTimeSpanTag) ([]*gqlmodel.RangedStatisticsEntries, error) { + if len(ranges) == 0 { + return nil, errors.New("ranges may not be empty") + } + var variables []interface{} + var rangesStrs []string + for _, r := range ranges { + if r.Start.Time().After(r.End.Time()) { + return nil, fmt.Errorf("range start must be before range end") + } + rangesStrs = append(rangesStrs, "(?, ?)") + variables = append(variables, r.Start.OmitTimeZone(), r.End.OmitTimeZone()) + } + queryRanges := strings.Join(rangesStrs, ", ") + + queryRequire, requireVars := build(requireTags, "1 = 1") + variables = append(variables, requireVars...) + + queryExclude, excludeVars := build(excludeTags, "1 != 1") + variables = append(variables, excludeVars...) + + query := fmt.Sprintf(` +WITH dates(query_start, query_end) AS ( + VALUES %s +) +SELECT query_start as query_start, + query_end as query_end, + key as key, + string_value as string_value, + sum(round((julianday(CASE WHEN end_user_time > query_end THEN query_end ELSE end_user_time END) - + julianday(CASE WHEN start_user_time < query_start THEN query_start ELSE start_user_time END)) + * 24 * 60 * 60, 0)) as time_spend_in_seconds +FROM dates + JOIN time_span_tags tst + JOIN ( + SELECT * + FROM time_spans tsx + WHERE (EXISTS(SELECT tstx.key + FROM time_span_tags as tstx + WHERE (tstx.time_span_id = tsx.id) + AND (%s))) + AND (NOT EXISTS(SELECT tstx.key + FROM time_span_tags as tstx + WHERE (tstx.time_span_id = id) + AND (%s))) +) as ts on ts.id = tst.time_span_id +WHERE (key in (?)) + AND (user_id = ?) + AND (start_user_time <= query_end AND end_user_time >= query_start) +GROUP BY query_start, + key, + string_value; +`, queryRanges, queryRequire, queryExclude) + variables = append(variables, tags, auth.GetUser(ctx).ID) + + var entries []statReturn + r.DB.Raw(query, variables...).Scan(&entries) + + statisticsEntries, err := group(entries) + if err != nil { + return nil, err + } + + statisticsEntries = fillEmptyRanges(ranges, statisticsEntries) + fillEmptyTags(statisticsEntries) + + return statisticsEntries, r.DB.Error +} + +func fillEmptyTags(statisticsEntries []*gqlmodel.RangedStatisticsEntries) { + lookup := make(map[string]struct{}) + for _, entry := range statisticsEntries { + for _, statEntry := range entry.Entries { + lookup[key(statEntry.Key, statEntry.Value)] = struct{}{} + } + } + for _, entry := range statisticsEntries { + existing := make(map[string]struct{}, len(lookup)) + for _, statEntry := range entry.Entries { + existing[key(statEntry.Key, statEntry.Value)] = struct{}{} + } + for identifier := range lookup { + if _, ok := existing[identifier]; !ok { + key, value := extract(identifier) + entry.Entries = append(entry.Entries, &gqlmodel.StatisticsEntry{Key: key, Value: value, TimeSpendInSeconds: 0}) + } + } + } +} + +func fillEmptyRanges(ranges []*gqlmodel.Range, statisticsEntries []*gqlmodel.RangedStatisticsEntries) []*gqlmodel.RangedStatisticsEntries { + result := make([]*gqlmodel.RangedStatisticsEntries, len(ranges)) + statsIdx := 0 + for idx, r := range ranges { + if statsIdx < len(statisticsEntries) && + statisticsEntries[statsIdx].Start.UTC().Equal(r.Start.UTC()) && + statisticsEntries[statsIdx].End.UTC().Equal(r.End.UTC()) { + result[idx] = statisticsEntries[statsIdx] + statsIdx++ + continue + } + + result[idx] = &gqlmodel.RangedStatisticsEntries{ + Start: model.Time(r.Start.UTC()), + End: model.Time(r.End.UTC()), + Entries: []*gqlmodel.StatisticsEntry{}, + } + } + return result +} + +type statReturn struct { + QueryStart string + QueryEnd string + Key string + StringValue string + TimeSpendInSeconds float64 +} + +func group(entries []statReturn) ([]*gqlmodel.RangedStatisticsEntries, error) { + stats := map[string]*gqlmodel.RangedStatisticsEntries{} + for _, entry := range entries { + id := entry.QueryStart + "/" + entry.QueryEnd + if _, ok := stats[id]; !ok { + start, err := time.ParseInLocation("2006-01-02 15:04:05Z07:00", entry.QueryStart, time.UTC) + if err != nil { + return nil, err + } + end, err := time.ParseInLocation("2006-01-02 15:04:05Z07:00", entry.QueryEnd, time.UTC) + if err != nil { + return nil, err + } + stats[id] = &gqlmodel.RangedStatisticsEntries{Start: model.Time(start), End: model.Time(end)} + } + stats[id].Entries = append(stats[id].Entries, &gqlmodel.StatisticsEntry{ + Key: entry.Key, + Value: entry.StringValue, + TimeSpendInSeconds: entry.TimeSpendInSeconds, + }) + } + + var result []*gqlmodel.RangedStatisticsEntries + for _, value := range stats { + result = append(result, value) + } + + sort.SliceStable(result, func(i, j int) bool { return result[i].Start.Time().Before(result[j].End.Time()) }) + return result, nil +} + +func build(tags []*gqlmodel.InputTimeSpanTag, noop string) (string, []interface{}) { + if len(tags) == 0 { + return noop, nil + } + + var have []string + var haveParams []interface{} + for _, tag := range tags { + have = append(have, "(tstx.key = ? AND tstx.string_value = ?)") + haveParams = append(haveParams, tag.Key, tag.Value) + } + + return strings.Join(have, " OR "), haveParams +} + +func key(key string, value string) string { + return fmt.Sprintf("%s:%s", key, value) +} + +func extract(id string) (string, string) { + split := strings.Split(id, ":") + return split[0], split[1] +} diff --git a/statistics/summary2.go b/statistics/summary2.go new file mode 100644 index 0000000..f04f6fc --- /dev/null +++ b/statistics/summary2.go @@ -0,0 +1,35 @@ +package statistics + +import ( + "context" + + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/setting" + "github.com/traggo/server/time" +) + +// Stats2 another version of the stats endpoint +func (r *ResolverForStatistics) Stats2(ctx context.Context, now model.Time, stats gqlmodel.InputStatsSelection) ([]*gqlmodel.RangedStatisticsEntries, error) { + + settings, err := setting.Get(ctx, r.DB) + if err != nil { + return nil, err + } + + var ranges []*gqlmodel.Range + + staticRanges, err := time.ParseRange(now.OmitTimeZone(), + time.RelativeRange{From: stats.Range.From, To: stats.Range.To}, + time.InternalInterval(stats.Interval), + settings.FirstDayOfTheWeekTimeWeekday(), + settings.LastDayOfTheWeekTimeWeekday()) + if err != nil { + return nil, err + } + for _, r := range staticRanges { + ranges = append(ranges, &gqlmodel.Range{Start: model.Time(r.From), End: model.Time(r.To)}) + } + + return r.Stats(ctx, ranges, stats.Tags, stats.ExcludeTags, stats.IncludeTags) +} diff --git a/statistics/summary_test.go b/statistics/summary_test.go new file mode 100644 index 0000000..140870d --- /dev/null +++ b/statistics/summary_test.go @@ -0,0 +1,322 @@ +package statistics + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestSummary_fails(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + resolver := ResolverForStatistics{DB: db.DB} + + _, err := resolver.Stats(fake.User(1), []*gqlmodel.Range{rangex("2019-06-11T10:00:00Z", "2019-06-11T09:00:00Z")}, []string{"type"}, nil, nil) + require.Error(t, err, "range start must be before range end ") +} + +func TestStats(t *testing.T) { + data := []*testEntry{ + name("simple"). + Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"))). + Keys("type"). + Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:00:10Z")). + Expected( + result("2019-06-11T10:00:00Z", "2019-06-11T10:00:10Z", + entry("type", "review", 10*time.Second))), + name("boundaries 1"). + Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"))). + Keys("type"). + Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:00:05Z")). + Expected( + result("2019-06-11T10:00:00Z", "2019-06-11T10:00:05Z", + entry("type", "review", 5*time.Second))), + name("boundaries 2"). + Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"))). + Keys("type"). + Ranges(rangex("2019-06-11T10:00:05Z", "2019-06-11T10:00:10Z")). + Expected( + result("2019-06-11T10:00:05Z", "2019-06-11T10:00:10Z", + entry("type", "review", 5*time.Second))), + name("boundaries 3"). + Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"))). + Keys("type"). + Ranges(rangex("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z")). + Expected( + result("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z", + entry("type", "review", 2*time.Second))), + name("exclude 1"). + Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"), tag("issue", "13"))). + Keys("type"). + Exclude(tag("issue", "13")). + Ranges(rangex("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z")). + Expected( + result("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z", []*gqlmodel.StatisticsEntry{}...)), + name("empty range"). + Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"), tag("issue", "13"))). + Keys("type"). + Ranges(rangex("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z"), + rangex("2019-06-12T10:00:04Z", "2019-06-12T10:00:06Z")). + Expected( + result("2019-06-11T10:00:04Z", "2019-06-11T10:00:06Z", entry("type", "review", 2*time.Second)), + result("2019-06-12T10:00:04Z", "2019-06-12T10:00:06Z", entry("type", "review", 0*time.Second))), + name("include 1"). + Load(ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"), tag("issue", "13"))). + Keys("type"). + Include(tag("issue", "13")). + Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:00:10Z")). + Expected( + result("2019-06-11T10:00:00Z", "2019-06-11T10:00:10Z", + entry("type", "review", 10*time.Second))), + name("multiple 1"). + Load( + ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"), tag("issue", "13")), + ts("2019-06-11T10:01:00Z", 10*time.Second, tag("type", "review"), tag("issue", "14")), + ).Keys("type"). + Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:02:00Z")). + Expected( + result("2019-06-11T10:00:00Z", "2019-06-11T10:02:00Z", + entry("type", "review", 20*time.Second))), + name("multiple 2"). + Load( + ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review"), tag("issue", "13")), + ts("2019-06-11T10:01:00Z", 10*time.Second, tag("type", "review"), tag("issue", "14")), + ).Keys("type"). + Exclude(tag("issue", "14")). + Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:02:00Z")). + Expected( + result("2019-06-11T10:00:00Z", "2019-06-11T10:02:00Z", + entry("type", "review", 10*time.Second))), + name("fill empty range"). + Load( + ts("2019-06-11T10:00:00Z", 10*time.Second, tag("type", "review")), + ).Keys("type"). + Ranges( + rangex("2019-06-10T09:58:00Z", "2019-06-10T09:59:59Z"), + rangex("2019-06-10T10:00:00Z", "2019-06-11T10:02:00Z")). + Expected( + result("2019-06-10T09:58:00Z", "2019-06-10T09:59:59Z", + entry("type", "review", 0*time.Second)), + result("2019-06-10T10:00:00Z", "2019-06-11T10:02:00Z", + entry("type", "review", 10*time.Second))), + name("multiple 3"). + Load( + ts("2019-06-11T10:00:00Z", 10*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")), + ts("2019-06-11T10:01:00Z", 60*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "12")), + ts("2019-06-11T10:02:00Z", 7*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")), + ts("2019-06-11T10:03:00Z", 14*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "15")), + ts("2019-06-11T10:04:00Z", 16*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "14")), + ts("2019-06-11T10:05:00Z", 33*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "10")), + ts("2019-06-11T10:05:00Z", 12*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "13")), + ts("2019-06-11T10:06:00Z", 90*time.Second, tag("type", "break")), + ts("2019-06-11T10:08:00Z", 33*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "10")), + ts("2019-06-11T10:09:00Z", 21*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "15")), + ts("2019-06-11T10:10:00Z", 18*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "12")), + ts("2019-06-11T10:11:00Z", 56*time.Second, tag("proj", "traggo"), tag("type", "review"), tag("issue", "11")), + ts("2019-06-11T10:12:00Z", 44*time.Second, tag("type", "break")), + ).Keys("type"). + Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z")). + Expected( + result("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z", + entry("type", "break", 134*time.Second), + entry("type", "review", 133*time.Second), + entry("type", "support", 45*time.Second), + entry("type", "work", 102*time.Second))), + name("multiple 4"). + Load( + ts("2019-06-11T10:00:00Z", 10*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")), + ts("2019-06-11T10:01:00Z", 60*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "12")), + ts("2019-06-11T10:02:00Z", 7*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")), + ts("2019-06-11T10:03:00Z", 14*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "15")), + ts("2019-06-11T10:04:00Z", 16*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "14")), + ts("2019-06-11T10:05:00Z", 33*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "10")), + ts("2019-06-11T10:05:00Z", 12*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "13")), + ts("2019-06-11T10:06:00Z", 90*time.Second, tag("type", "break")), + ts("2019-06-11T10:08:00Z", 32*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "10")), + ts("2019-06-11T10:09:00Z", 21*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "15")), + ts("2019-06-11T10:10:00Z", 18*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "12")), + ts("2019-06-11T10:11:00Z", 56*time.Second, tag("proj", "traggo"), tag("type", "review"), tag("issue", "11")), + ts("2019-06-11T10:12:00Z", 44*time.Second, tag("type", "break")), + ).Keys("type", "issue"). + Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z")). + Expected( + result("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z", + entry("issue", "10", 65*time.Second), + entry("issue", "11", 56*time.Second), + entry("issue", "12", 78*time.Second), + entry("issue", "13", 29*time.Second), + entry("issue", "14", 16*time.Second), + entry("issue", "15", 35*time.Second), + entry("type", "break", 134*time.Second), + entry("type", "review", 133*time.Second), + entry("type", "support", 45*time.Second), + entry("type", "work", 101*time.Second))), + name("multiple 5"). + Load( + ts("2019-06-11T10:00:00Z", 10*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")), + ts("2019-06-11T10:01:00Z", 60*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "12")), + ts("2019-06-11T10:02:00Z", 7*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")), + ts("2019-06-11T10:03:00Z", 14*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "15")), + ts("2019-06-11T10:04:00Z", 16*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "14")), + ts("2019-06-11T10:05:00Z", 33*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "10")), + ts("2019-06-11T10:05:00Z", 12*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "13")), + ts("2019-06-11T10:06:00Z", 90*time.Second, tag("type", "break")), + ts("2019-06-11T10:08:00Z", 32*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "10")), + ts("2019-06-11T10:09:00Z", 21*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "15")), + ts("2019-06-11T10:10:00Z", 18*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "12")), + ts("2019-06-11T10:11:00Z", 56*time.Second, tag("proj", "traggo"), tag("type", "review"), tag("issue", "11")), + ts("2019-06-11T10:12:00Z", 44*time.Second, tag("type", "break")), + ).Keys("type"). + Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z")). + Include(tag("issue", "13"), tag("issue", "12")). + Expected( + result("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z", + entry("type", "review", 77*time.Second), + entry("type", "support", 12*time.Second), + entry("type", "work", 18*time.Second))), + name("multiple 6"). + Load( + ts("2019-06-11T10:00:00Z", 10*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")), + ts("2019-06-11T10:01:00Z", 60*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "12")), + ts("2019-06-11T10:02:00Z", 7*time.Second, tag("proj", "gotify"), tag("type", "review"), tag("issue", "13")), + ts("2019-06-11T10:03:00Z", 14*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "15")), + ts("2019-06-11T10:04:00Z", 16*time.Second, tag("proj", "gotify"), tag("type", "work"), tag("issue", "14")), + ts("2019-06-11T10:05:00Z", 33*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "10")), + ts("2019-06-11T10:05:00Z", 12*time.Second, tag("proj", "gotify"), tag("type", "support"), tag("issue", "13")), + ts("2019-06-11T10:06:00Z", 90*time.Second, tag("type", "break")), + ts("2019-06-11T10:08:00Z", 32*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "10")), + ts("2019-06-11T10:09:00Z", 21*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "15")), + ts("2019-06-11T10:10:00Z", 18*time.Second, tag("proj", "traggo"), tag("type", "work"), tag("issue", "12")), + ts("2019-06-11T10:11:00Z", 56*time.Second, tag("proj", "traggo"), tag("type", "review"), tag("issue", "11")), + ts("2019-06-11T10:12:00Z", 44*time.Second, tag("type", "break")), + ).Keys("type"). + Ranges(rangex("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z")). + Exclude(tag("issue", "10"), tag("issue", "11"), tag("issue", "14"), tag("issue", "15")). + Expected( + result("2019-06-11T10:00:00Z", "2019-06-11T10:15:00Z", + entry("type", "break", 134*time.Second), + entry("type", "review", 77*time.Second), + entry("type", "support", 12*time.Second), + entry("type", "work", 18*time.Second))), + } + + for _, entry := range data { + t.Run(entry.name, func(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + db.User(1) + for _, timespan := range entry.timespans { + db.Create(×pan) + } + + resolver := ResolverForStatistics{DB: db.DB} + + stats, err := resolver.Stats(fake.User(1), entry.ranges, entry.keys, entry.exclude, entry.include) + require.NoError(t, err) + require.Equal(t, entry.expected, stats) + require.Len(t, stats, len(entry.expected)) + }) + } +} + +func ts(start string, d time.Duration, tags ...model.TimeSpanTag) model.TimeSpan { + end := test.Time(start).Add(d) + return model.TimeSpan{ + StartUserTime: test.Time(start), + EndUserTime: &end, + UserID: 1, + Tags: tags, + } +} + +type testEntry struct { + timespans []model.TimeSpan + keys []string + exclude []*gqlmodel.InputTimeSpanTag + include []*gqlmodel.InputTimeSpanTag + name string + ranges []*gqlmodel.Range + expected []*gqlmodel.RangedStatisticsEntries +} + +func name(name string) *testEntry { + return &testEntry{ + name: name, + } +} + +func (e *testEntry) Expected(expected ...*gqlmodel.RangedStatisticsEntries) *testEntry { + e.expected = expected + return e +} + +func (e *testEntry) Keys(keys ...string) *testEntry { + e.keys = keys + return e +} + +func (e *testEntry) Load(timespans ...model.TimeSpan) *testEntry { + e.timespans = timespans + return e +} + +func (e *testEntry) Ranges(ranges ...*gqlmodel.Range) *testEntry { + e.ranges = ranges + return e +} + +func (e *testEntry) Include(includes ...model.TimeSpanTag) *testEntry { + for _, entry := range includes { + e.include = append(e.include, &gqlmodel.InputTimeSpanTag{ + Key: entry.Key, + Value: entry.StringValue, + }) + } + return e +} +func (e *testEntry) Exclude(excludes ...model.TimeSpanTag) *testEntry { + for _, entry := range excludes { + e.exclude = append(e.exclude, &gqlmodel.InputTimeSpanTag{ + Key: entry.Key, + Value: entry.StringValue, + }) + } + return e +} + +func tag(key string, value string) model.TimeSpanTag { + return model.TimeSpanTag{ + Key: key, + StringValue: value, + } +} + +func rangex(start, stop string) *gqlmodel.Range { + return &gqlmodel.Range{ + Start: test.ModelTime(start), + End: test.ModelTime(stop), + } +} + +func result(start, stop string, entries ...*gqlmodel.StatisticsEntry) *gqlmodel.RangedStatisticsEntries { + return &gqlmodel.RangedStatisticsEntries{ + Start: test.ModelTimeUTC(start), + End: test.ModelTimeUTC(stop), + Entries: entries, + } +} + +func entry(key string, value string, duration time.Duration) *gqlmodel.StatisticsEntry { + return &gqlmodel.StatisticsEntry{ + Key: key, + Value: value, + TimeSpendInSeconds: duration.Seconds(), + } +} diff --git a/tag/create.go b/tag/create.go new file mode 100644 index 0000000..cb482c0 --- /dev/null +++ b/tag/create.go @@ -0,0 +1,38 @@ +package tag + +import ( + "context" + "fmt" + "strings" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// CreateTag creates a tag. +func (r *ResolverForTag) CreateTag(ctx context.Context, key string, color string) (*gqlmodel.TagDefinition, error) { + if strings.TrimSpace(key) == "" { + return nil, fmt.Errorf("tag must not be empty") + } + if strings.Contains(key, " ") { + return nil, fmt.Errorf("tag must not contain spaces") + } + + userID := auth.GetUser(ctx).ID + definition := &model.TagDefinition{ + Key: strings.ToLower(key), + Color: color, + UserID: userID, + } + + if !r.DB.Where("user_id = ?", userID).Where("key = ?", strings.ToLower(key)).Find(new(model.TagDefinition)).RecordNotFound() { + return nil, fmt.Errorf("tag with key '%s' does already exist", definition.Key) + } + + create := r.DB.Create(&definition) + gqlTag := &gqlmodel.TagDefinition{} + copier.Copy(gqlTag, definition) + return gqlTag, create.Error +} diff --git a/tag/create_test.go b/tag/create_test.go new file mode 100644 index 0000000..78c961d --- /dev/null +++ b/tag/create_test.go @@ -0,0 +1,101 @@ +package tag + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestGQL_CreateTag_succeeds_addsTag(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + + resolver := ResolverForTag{DB: db.DB} + tag, err := resolver.CreateTag(fake.User(5), "new_tag", "#fff") + + require.Nil(t, err) + expected := &gqlmodel.TagDefinition{ + Key: "new_tag", + Color: "#fff", + } + require.Equal(t, expected, tag) + assertTagExist(t, db, model.TagDefinition{ + Key: "new_tag", + Color: "#fff", + UserID: 5, + }) + assertTagCount(t, db, 1) +} + +func TestGQL_CreateTag_fails_tagAlreadyExists(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + db.Create(&model.TagDefinition{Key: "existing_tag", Color: "#fff", UserID: 5}) + + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.CreateTag(fake.User(5), "existing_tag", "#fff") + + require.EqualError(t, err, "tag with key 'existing_tag' does already exist") + assertTagCount(t, db, 1) +} + +func TestGQL_CreateTag_fails_tagAlreadyExists_caseInsensitive(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + db.Create(&model.TagDefinition{Key: "tag", Color: "#fff", UserID: 5}) + + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.CreateTag(fake.User(5), "Tag", "#fff") + + require.EqualError(t, err, "tag with key 'tag' does already exist") + assertTagCount(t, db, 1) +} + +func TestGQL_CreateTag_succeeds_existingTagForOtherUser(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(4) + db.User(5) + db.Create(&model.TagDefinition{Key: "existing_tag", Color: "#fff", UserID: 4}) + + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.CreateTag(fake.User(5), "existing_tag", "#xxx") + + assert.Nil(t, err) + assertTagCount(t, db, 2) + assertTagExist(t, db, model.TagDefinition{ + Key: "existing_tag", + Color: "#xxx", + UserID: 5, + }) +} + +func TestGQL_CreateTag_fails_empty(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.CreateTag(fake.User(5), "", "#fff") + + require.EqualError(t, err, "tag must not be empty") + assertTagCount(t, db, 0) +} + +func TestGQL_CreateTag_fails_includes_space(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.CreateTag(fake.User(5), "test tag", "#fff") + + require.EqualError(t, err, "tag must not contain spaces") + assertTagCount(t, db, 0) +} diff --git a/tag/get.go b/tag/get.go new file mode 100644 index 0000000..4739b37 --- /dev/null +++ b/tag/get.go @@ -0,0 +1,30 @@ +package tag + +import ( + "context" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// Tags returns all tags. +func (r *ResolverForTag) Tags(ctx context.Context) ([]*gqlmodel.TagDefinition, error) { + var tags []model.TagDefinition + userID := auth.GetUser(ctx).ID + + timeSpansIdsOfUser := r.DB.Model(new(model.TimeSpan)). + Select("id"). + Where(&model.TimeSpan{UserID: userID}). + SubQuery() + usages := r.DB.Select("COUNT (*)").Where("time_span_tags.time_span_id in ?", timeSpansIdsOfUser). + Where("tag_definitions.key = time_span_tags.key"). + Model(new(model.TimeSpanTag)). + Group("time_span_tags.key"). + SubQuery() + find := r.DB.Select("tag_definitions.*, ? as usages", usages).Where("user_id = ?", userID).Order("usages desc").Find(&tags) + result := []*gqlmodel.TagDefinition{} + copier.Copy(&result, &tags) + return result, find.Error +} diff --git a/tag/get_test.go b/tag/get_test.go new file mode 100644 index 0000000..4515987 --- /dev/null +++ b/tag/get_test.go @@ -0,0 +1,36 @@ +package tag + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestGQL_Tags(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + left := db.User(5) + right := db.User(2) + left.NewTagDefinition("my tag") + left.NewTagDefinition("my tag 2") + left.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z").Tag("my tag", "value") + left.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z").Tag("my tag", "value").Tag("my tag 2", "v") + left.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z").Tag("my tag", "value").Tag("my tag 2", "v") + right.NewTagDefinition("my tag") + right.NewTagDefinition("my tag 2") + right.NewTagDefinition("my tag 5") + right.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z").Tag("my tag", "value").Tag("my tag 2", "v") + + resolver := ResolverForTag{DB: db.DB} + tags, err := resolver.Tags(fake.User(left.User.ID)) + + require.Nil(t, err) + expected := []*gqlmodel.TagDefinition{ + {Key: "my tag", Usages: 3}, + {Key: "my tag 2", Usages: 2}, + } + require.Equal(t, expected, tags) +} diff --git a/tag/remove.go b/tag/remove.go new file mode 100644 index 0000000..5546d58 --- /dev/null +++ b/tag/remove.go @@ -0,0 +1,60 @@ +package tag + +import ( + "context" + "fmt" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// RemoveTag removes a tag. +func (r *ResolverForTag) RemoveTag(ctx context.Context, key string) (*gqlmodel.TagDefinition, error) { + tag := model.TagDefinition{} + userID := auth.GetUser(ctx).ID + if r.DB.Where(&model.TagDefinition{UserID: userID, Key: key}).Find(&tag).RecordNotFound() { + return nil, fmt.Errorf("tag with key '%s' does not exist", key) + } + + usedInEntries := []model.DashboardEntry{} + if err := r.DB. + Joins("INNER JOIN dashboards ON dashboard_entries.dashboard_id = dashboards.id"). + Where("dashboards.user_id = ?", userID). + Where("keys LIKE ? OR keys LIKE ? OR keys LIKE ?", "%"+key, "%"+key+"%", key+"%"). + Find(&usedInEntries).Error; err != nil { + return nil, err + } + + if len(usedInEntries) > 0 { + dashboard := &model.Dashboard{ID: usedInEntries[0].DashboardID} + r.DB.Find(dashboard) + return nil, fmt.Errorf("tag '%s' is used in dashboard '%s' entry '%s', remove this reference before deleting the tag", key, dashboard.Name, usedInEntries[0].Title) + } + + tx := r.DB.Begin() + if err := tx.Where(model.TagDefinition{Key: key, UserID: userID}). + Delete(new(model.TagDefinition)).Error; err != nil { + tx.Rollback() + return nil, err + } + + timeSpansIdsOfUser := tx.Model(new(model.TimeSpan)). + Select("id"). + Where(&model.TimeSpan{UserID: userID}). + SubQuery() + + if err := tx. + Where("time_span_id in ?", timeSpansIdsOfUser). + Where(&model.TimeSpanTag{Key: key}). + Delete(new(model.TimeSpanTag)).Error; err != nil { + tx.Rollback() + return nil, err + } + + remove := tx.Commit() + gqlTag := &gqlmodel.TagDefinition{} + copier.Copy(gqlTag, &tag) + return gqlTag, remove.Error +} diff --git a/tag/remove_test.go b/tag/remove_test.go new file mode 100644 index 0000000..64eb00e --- /dev/null +++ b/tag/remove_test.go @@ -0,0 +1,106 @@ +package tag + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestGQL_RemoveTag_succeeds_removesTag(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(3) + user.NewTagDefinition("existing") + + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.RemoveTag(fake.User(3), "existing") + require.Nil(t, err) + user.AssertHasTagDefinition("existing", false) +} + +func TestRemove_referencedInDashboardEntry(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + left := db.User(5) + left.NewTagDefinition("coolio") + dashboard := left.Dashboard("yeah") + dashboard.Entry("entry") + entry := dashboard.Dashboard.Entries[0] + entry.Keys = "abc,coolio,chicken" + db.Save(&entry) + + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.RemoveTag(fake.User(left.User.ID), "coolio") + require.EqualError(t, err, "tag 'coolio' is used in dashboard 'yeah' entry 'entry', remove this reference before deleting the tag") +} + +func TestGQL_RemoveTag_succeeds_removesTimespans(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + left := db.User(3) + right := db.User(4) + left.NewTagDefinition("tag") + right.NewTagDefinition("tag") + leftTs := left.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z") + leftTs.Tag("tag", "def") + rightTs := right.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z") + rightTs.Tag("tag", "def") + + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.RemoveTag(fake.User(left.User.ID), "tag") + require.Nil(t, err) + + assertTagCount(t, db, 1) + + left.AssertHasTagDefinition("tag", false) + right.AssertHasTagDefinition("tag", true) + + leftTs.AssertExists(true).AssertHasTag("tag", "def", false) + rightTs.AssertExists(true).AssertHasTag("tag", "def", true) +} + +func TestGQL_RemoveTag_fails_notExistingTag(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(3) + + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.RemoveTag(fake.User(3), "not existing") + require.EqualError(t, err, "tag with key 'not existing' does not exist") +} + +func TestGQL_RemoveTag_fails_notPermission(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(3).NewTagDefinition("existing") + db.User(5) + + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.RemoveTag(fake.User(5), "existing") + require.EqualError(t, err, "tag with key 'existing' does not exist") +} + +func TestRemove_crossUserIsolation(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + userA := db.User(5) + userB := db.User(2) + + userA.NewTagDefinition("project") + userB.NewTagDefinition("project") + + dashboardB := userB.Dashboard("secretDashboard") + dashboardB.Entry("secretEntry") + entryB := dashboardB.Dashboard.Entries[0] + entryB.Keys = "project,secret" + db.Save(&entryB) + + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.RemoveTag(fake.User(userA.User.ID), "project") + require.NoError(t, err, "removing user's own tag should succeed even if another user has a dashboard entry with that tag") + + userA.AssertHasTagDefinition("project", false) + userB.AssertHasTagDefinition("project", true) +} diff --git a/tag/suggest.go b/tag/suggest.go new file mode 100644 index 0000000..2f1bb12 --- /dev/null +++ b/tag/suggest.go @@ -0,0 +1,19 @@ +package tag + +import ( + "context" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// SuggestTag suggests a tag. +func (r *ResolverForTag) SuggestTag(ctx context.Context, query string) ([]*gqlmodel.TagDefinition, error) { + var suggestions []model.TagDefinition + find := r.DB.Where("user_id = ?", auth.GetUser(ctx).ID).Where("Key LIKE ?", query+"%").Find(&suggestions) + var result []*gqlmodel.TagDefinition + copier.Copy(&result, &suggestions) + return result, find.Error +} diff --git a/tag/suggest_test.go b/tag/suggest_test.go new file mode 100644 index 0000000..09bbf72 --- /dev/null +++ b/tag/suggest_test.go @@ -0,0 +1,47 @@ +package tag + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestGQL_SuggestTag_matchesTags(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(1) + db.User(2) + resolver := ResolverForTag{DB: db.DB} + db.Create(&model.TagDefinition{Key: "project", Color: "#fff", UserID: 1}) + db.Create(&model.TagDefinition{Key: "project2", Color: "#fff", UserID: 2}) + db.Create(&model.TagDefinition{Key: "priority", Color: "#fff", UserID: 1}) + db.Create(&model.TagDefinition{Key: "wood", Color: "#fff", UserID: 1}) + + tags, err := resolver.SuggestTag(fake.User(1), "pr") + + require.Nil(t, err) + expected := []*gqlmodel.TagDefinition{ + {Key: "project", Color: "#fff"}, + {Key: "priority", Color: "#fff"}, + } + require.Equal(t, expected, tags) +} + +func TestGQL_SuggestTag_noMatchingTags(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(1) + resolver := ResolverForTag{DB: db.DB} + db.Create(&model.TagDefinition{Key: "project", Color: "#fff", UserID: 1}) + db.Create(&model.TagDefinition{Key: "wood", Color: "#fff", UserID: 1}) + + tags, err := resolver.SuggestTag(fake.User(1), "fire") + + require.Nil(t, err) + expected := []*gqlmodel.TagDefinition{} + require.Equal(t, expected, tags) +} diff --git a/tag/tagresolver.go b/tag/tagresolver.go new file mode 100644 index 0000000..1462eef --- /dev/null +++ b/tag/tagresolver.go @@ -0,0 +1,8 @@ +package tag + +import "github.com/jinzhu/gorm" + +// ResolverForTag resolves tag specific things. +type ResolverForTag struct { + DB *gorm.DB +} diff --git a/tag/update.go b/tag/update.go new file mode 100644 index 0000000..9614b30 --- /dev/null +++ b/tag/update.go @@ -0,0 +1,86 @@ +package tag + +import ( + "context" + "fmt" + "strings" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// UpdateTag updates a tag. +func (r *ResolverForTag) UpdateTag(ctx context.Context, key string, newKey *string, color string) (*gqlmodel.TagDefinition, error) { + tag := model.TagDefinition{} + userID := auth.GetUser(ctx).ID + if r.DB.Where(&model.TagDefinition{UserID: userID, Key: key}).Find(&tag).RecordNotFound() { + return nil, fmt.Errorf("tag with key '%s' does not exist", key) + } + + tx := r.DB.Begin() + + newValue := model.TagDefinition{ + Key: strings.ToLower(key), + Color: color, + UserID: userID, + } + + if newKey != nil && *newKey != key { + if strings.Contains(*newKey, " ") { + tx.Rollback() + return nil, fmt.Errorf("tag must not contain spaces") + } + *newKey = strings.ToLower(*newKey) + newValue.Key = *newKey + timeSpansIdsOfUser := tx.Model(new(model.TimeSpan)). + Select("id"). + Where(&model.TimeSpan{UserID: userID}). + SubQuery() + + if err := tx. + Model(new(model.TimeSpanTag)). + Where("time_span_id in ?", timeSpansIdsOfUser). + Where(&model.TimeSpanTag{Key: key}). + Updates(&model.TimeSpanTag{Key: *newKey}).Error; err != nil { + tx.Rollback() + return nil, err + } + usedInEntries := []model.DashboardEntry{} + + if err := tx. + Joins("INNER JOIN dashboards ON dashboard_entries.dashboard_id = dashboards.id"). + Where("dashboards.user_id = ?", userID). + Where("keys LIKE ? OR keys LIKE ? OR keys LIKE ?", "%"+key, "%"+key+"%", key+"%"). + Find(&usedInEntries).Error; err != nil { + tx.Rollback() + return nil, err + } + + for _, entry := range usedInEntries { + tags := strings.Split(entry.Keys, ",") + for index, tagInEntry := range tags { + if tagInEntry == key { + tags[index] = *newKey + } + } + entry.Keys = strings.Join(tags, ",") + if err := tx.Save(&entry).Error; err != nil { + tx.Rollback() + return nil, err + } + } + } + + if err := tx.Model(new(model.TagDefinition)).Where(&model.TagDefinition{UserID: userID, Key: key}).Updates(&newValue).Error; err != nil { + tx.Rollback() + return nil, err + } + + update := tx.Commit() + + gqlTag := &gqlmodel.TagDefinition{} + copier.Copy(gqlTag, &newValue) + return gqlTag, update.Error +} diff --git a/tag/update_test.go b/tag/update_test.go new file mode 100644 index 0000000..3f78f67 --- /dev/null +++ b/tag/update_test.go @@ -0,0 +1,158 @@ +package tag + +import ( + "testing" + + "github.com/magiconair/properties/assert" + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestUpdate_withKey(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + left := db.User(5) + right := db.User(2) + left.NewTagDefinition("coolio") + right.NewTagDefinition("coolio") + leftTs := left.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z") + leftTs.Tag("coolio", "mama") + rightTs := right.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z") + rightTs.Tag("coolio", "mama") + + resolver := ResolverForTag{DB: db.DB} + newTagName := "mega" + tag, err := resolver.UpdateTag(fake.User(left.User.ID), "coolio", &newTagName, "#abc") + require.NoError(t, err) + require.Equal(t, &gqlmodel.TagDefinition{ + Color: "#abc", + Key: "mega", + }, tag) + left.AssertHasTagDefinition("coolio", false).AssertHasTagDefinition("mega", true) + right.AssertHasTagDefinition("coolio", true).AssertHasTagDefinition("mega", false) + leftTs.AssertHasTag("mega", "mama", true).AssertHasTag("coolio", "mama", false) + rightTs.AssertHasTag("coolio", "mama", true).AssertHasTag("mega", "mama", false) +} + +func TestUpdate_lowercases(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(5) + user.NewTagDefinition("coolio") + ts := user.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z") + ts.Tag("coolio", "mama") + + resolver := ResolverForTag{DB: db.DB} + newTagName := "Mega" + tag, err := resolver.UpdateTag(fake.User(user.User.ID), "coolio", &newTagName, "#abc") + require.NoError(t, err) + require.Equal(t, &gqlmodel.TagDefinition{ + Color: "#abc", + Key: "mega", + }, tag) + user.AssertHasTagDefinition("coolio", false).AssertHasTagDefinition("mega", true) + ts.AssertHasTag("mega", "mama", true).AssertHasTag("coolio", "mama", false) +} + +func TestUpdate_disallow_space(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(5) + user.NewTagDefinition("coolio") + + resolver := ResolverForTag{DB: db.DB} + newTagName := "the coolio" + _, err := resolver.UpdateTag(fake.User(user.User.ID), "coolio", &newTagName, "#abc") + require.EqualError(t, err, "tag must not contain spaces") +} + +func TestUpdate_withoutKey(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + left := db.User(5) + right := db.User(2) + left.NewTagDefinition("coolio") + right.NewTagDefinition("coolio") + leftTs := left.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z") + leftTs.Tag("coolio", "mama") + rightTs := right.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z") + rightTs.Tag("coolio", "mama") + + resolver := ResolverForTag{DB: db.DB} + tag, err := resolver.UpdateTag(fake.User(left.User.ID), "coolio", nil, "#abc") + require.NoError(t, err) + assert.Equal(t, &gqlmodel.TagDefinition{ + Color: "#abc", + Key: "coolio", + }, tag) +} + +func TestUpdate_dashboardEntryKey(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + left := db.User(5) + left.NewTagDefinition("coolio") + dashboard := left.Dashboard("yeah") + dashboard.Entry("entry") + entry := dashboard.Dashboard.Entries[0] + entry.Keys = "abc,coolio,chicken" + db.Save(&entry) + + newTag := "yes" + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.UpdateTag(fake.User(left.User.ID), "coolio", &newTag, "#abc") + require.NoError(t, err) + + db.Find(&entry) + require.Equal(t, "abc,yes,chicken", entry.Keys) +} + +func TestUpdate_noPermissions(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + left := db.User(5) + right := db.User(2) + right.NewTagDefinition("coolio") + rightTs := right.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z") + rightTs.Tag("coolio", "mama") + + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.UpdateTag(fake.User(left.User.ID), "coolio", nil, "#abc") + require.EqualError(t, err, "tag with key 'coolio' does not exist") + right.AssertHasTagDefinition("coolio", true) +} + +func TestUpdate_dashboardEntryKey_crossUserIsolation(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + userA := db.User(5) + userB := db.User(2) + + userA.NewTagDefinition("project") + userB.NewTagDefinition("project") + + dashboardA := userA.Dashboard("dashboardA") + dashboardA.Entry("entryA") + entryA := dashboardA.Dashboard.Entries[0] + entryA.Keys = "project,task1" + db.Save(&entryA) + + dashboardB := userB.Dashboard("dashboardB") + dashboardB.Entry("entryB") + entryB := dashboardB.Dashboard.Entries[0] + entryB.Keys = "project,task2" + db.Save(&entryB) + + newTag := "newproject" + resolver := ResolverForTag{DB: db.DB} + _, err := resolver.UpdateTag(fake.User(userA.User.ID), "project", &newTag, "#abc") + require.NoError(t, err) + + db.Find(&entryA) + require.Equal(t, "newproject,task1", entryA.Keys, "user A's entry should be updated") + + db.Find(&entryB) + require.Equal(t, "project,task2", entryB.Keys, "user B's entry should NOT be modified") +} diff --git a/tag/utils_test.go b/tag/utils_test.go new file mode 100644 index 0000000..8d0b00f --- /dev/null +++ b/tag/utils_test.go @@ -0,0 +1,23 @@ +package tag + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +func assertTagExist(t *testing.T, db *test.Database, expected model.TagDefinition) { + foundUser := new(model.TagDefinition) + find := db.Where("key = ?", expected.Key).Find(foundUser) + require.Nil(t, find.Error) + require.NotNil(t, foundUser) + require.Equal(t, expected, *foundUser) +} + +func assertTagCount(t *testing.T, db *test.Database, expected int) { + count := new(int) + db.Model(new(model.TagDefinition)).Count(count) + require.Equal(t, expected, *count) +} diff --git a/test/db.go b/test/db.go new file mode 100644 index 0000000..2514b80 --- /dev/null +++ b/test/db.go @@ -0,0 +1,270 @@ +package test + +import ( + "fmt" + + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" + "github.com/traggo/server/database" + "github.com/traggo/server/model" +) + +// InMemoryDB create a in memory database for testing. +func InMemoryDB(t assert.TestingT) *Database { + db, err := database.New("sqlite3", "file::memory:?mode=memory&cache=shared") + assert.Nil(t, err) + return &Database{DB: db, t: t} +} + +// Database wraps the gorm.DB and provides helper methods +type Database struct { + *gorm.DB + t assert.TestingT +} + +// User creates a user +func (d *Database) User(id int) *UserWithDatabase { + user := d.NewUser(id, fmt.Sprint("test", id), false) + return &UserWithDatabase{ + User: user, + DB: d.DB, + t: d.t, + } +} + +// UserWithDatabase wraps gorm.DB and provides helper methods +type UserWithDatabase struct { + User model.User + *gorm.DB + t assert.TestingT +} + +// NewDevice creates a device. +func (d *UserWithDatabase) NewDevice(id int, token string, name string) model.Device { + device := model.Device{ + ID: id, + Token: token, + UserID: d.User.ID, + Name: name, + Type: model.TypeNoExpiry, + ActiveAt: Time("2009-06-30T18:30:00Z"), + CreatedAt: Time("2009-06-30T18:30:00Z"), + } + d.Create(&device) + return device +} + +// NewTagDefinition creates a tag definition. +func (d *UserWithDatabase) NewTagDefinition(key string) model.TagDefinition { + tagDefinition := model.TagDefinition{ + UserID: d.User.ID, + Key: key, + } + d.Create(&tagDefinition) + return tagDefinition +} + +// AssertHasTagDefinition asserts if a tag definition exists. +func (d *UserWithDatabase) AssertHasTagDefinition(key string, exist bool) *UserWithDatabase { + existActual := !d.DB. + Where(&model.TagDefinition{Key: key, UserID: d.User.ID}). + Find(new(model.TagDefinition)). + RecordNotFound() + assert.True(d.t, exist == existActual) + return d +} + +// AssertHasDevice asserts if a device exists. +func (d *UserWithDatabase) AssertHasDevice(name string, exist bool) *UserWithDatabase { + existActual := !d.DB. + Where(&model.Device{Name: name, UserID: d.User.ID}). + Find(new(model.Device)). + RecordNotFound() + assert.True(d.t, exist == existActual) + return d +} + +// NewUser creates a user +func (d *Database) NewUser(id int, name string, admin bool) model.User { + return d.NewUserPass(id, name, []byte{1, 2, 3}, admin) +} + +// NewUserPass creates a user +func (d *Database) NewUserPass(id int, name string, pass []byte, admin bool) model.User { + user := model.User{ + ID: id, + Name: name, + Pass: pass, + Admin: admin, + } + d.Create(&user) + return user +} + +// AssertExists asserts if the tag exists or not. +func (d *UserWithDatabase) AssertExists(exist bool) *UserWithDatabase { + existActual := !d.DB. + Where(&model.User{ID: d.User.ID}). + Find(new(model.User)). + RecordNotFound() + assert.True(d.t, exist == existActual) + return d +} + +// RunningTimeSpan adds a time span without end. +func (d *UserWithDatabase) RunningTimeSpan(from string) *TimeSpanWithDatabase { + timeSpan := model.TimeSpan{ + UserID: d.User.ID, + StartUserTime: ModelTime(from).OmitTimeZone(), + StartUTC: ModelTime(from).UTC(), + } + + d.DB.Create(&timeSpan) + + return &TimeSpanWithDatabase{ + User: d.User, + TimeSpan: timeSpan, + DB: d.DB, + t: d.t, + } +} + +// Dashboard adds a dashboard. +func (d *UserWithDatabase) Dashboard(name string) *DashboardWithDatabase { + dashboard := model.Dashboard{ + UserID: d.User.ID, + Name: name, + } + + d.DB.Create(&dashboard) + + return &DashboardWithDatabase{ + User: d.User, + Dashboard: dashboard, + DB: d.DB, + t: d.t, + } +} + +// Range adds a range to the dashboard. +func (d *DashboardWithDatabase) Range(name string) *DashboardWithDatabase { + dbRange := model.DashboardRange{ + DashboardID: d.Dashboard.ID, + Name: name, + From: "now-1d", + To: "now", + } + d.Dashboard.Ranges = append(d.Dashboard.Ranges, dbRange) + d.DB.Save(&dbRange) + return d +} + +// Entry adds an entry to the dashboard. +func (d *DashboardWithDatabase) Entry(name string) *DashboardWithDatabase { + entry := model.DashboardEntry{ + DashboardID: d.Dashboard.ID, + Title: name, + } + d.Dashboard.Entries = append(d.Dashboard.Entries, entry) + d.DB.Save(&entry) + return d +} + +// AssertExists asserts if the dashboard exists or not. +func (d *DashboardWithDatabase) AssertExists(exist bool) *DashboardWithDatabase { + existActual := !d.DB. + Where(&model.Dashboard{ID: d.Dashboard.ID}). + Find(new(model.Dashboard)). + RecordNotFound() + assert.True(d.t, exist == existActual) + return d +} + +// AssertHasEntry asserts if the entry exists or not. +func (d *DashboardWithDatabase) AssertHasEntry(name string, exist bool) *DashboardWithDatabase { + existActual := !d.DB. + Where(&model.DashboardEntry{DashboardID: d.Dashboard.ID, Title: name}). + Find(new(model.DashboardEntry)). + RecordNotFound() + assert.True(d.t, exist == existActual) + return d +} + +// AssertHasRange asserts if the range exists or not. +func (d *DashboardWithDatabase) AssertHasRange(name string, exist bool) *DashboardWithDatabase { + existActual := !d.DB. + Where(&model.DashboardRange{DashboardID: d.Dashboard.ID, Name: name}). + Find(new(model.DashboardRange)). + RecordNotFound() + assert.True(d.t, exist == existActual) + return d +} + +// TimeSpan adds a time span. +func (d *UserWithDatabase) TimeSpan(from string, to string) *TimeSpanWithDatabase { + wrapper := d.RunningTimeSpan(from) + userTime := ModelTime(to).OmitTimeZone() + wrapper.TimeSpan.EndUserTime = &userTime + utc := ModelTime(to).UTC() + wrapper.TimeSpan.EndUTC = &utc + d.DB.Save(&wrapper.TimeSpan) + return wrapper +} + +// TimeSpanWithDatabase wraps gorm.DB and provides helper methods +type TimeSpanWithDatabase struct { + User model.User + TimeSpan model.TimeSpan + t assert.TestingT + *gorm.DB +} + +// AssertHasTag asserts if the tag exists or not. +func (d *TimeSpanWithDatabase) AssertHasTag(key, value string, exist bool) *TimeSpanWithDatabase { + existActual := !d.DB. + Where(&model.TimeSpanTag{Key: key, StringValue: value, TimeSpanID: d.TimeSpan.ID}). + Find(new(model.TimeSpanTag)). + RecordNotFound() + assert.True(d.t, exist == existActual) + return d +} + +// AssertHasTagIgnoreValue asserts if the tag exists or not. +func (d *TimeSpanWithDatabase) AssertHasTagIgnoreValue(key string, exist bool) *TimeSpanWithDatabase { + existActual := !d.DB. + Where(&model.TimeSpanTag{Key: key, TimeSpanID: d.TimeSpan.ID}). + Find(new(model.TimeSpanTag)). + RecordNotFound() + assert.True(d.t, exist == existActual) + return d +} + +// AssertExists asserts if the tag exists or not. +func (d *TimeSpanWithDatabase) AssertExists(exist bool) *TimeSpanWithDatabase { + existActual := !d.DB. + Where(&model.TimeSpan{ID: d.TimeSpan.ID}). + Find(new(model.TimeSpan)). + RecordNotFound() + assert.True(d.t, exist == existActual) + return d +} + +// Tag adds a tag to the time span. +func (d *TimeSpanWithDatabase) Tag(key string, stringValue string) *TimeSpanWithDatabase { + tag := model.TimeSpanTag{ + TimeSpanID: d.TimeSpan.ID, + Key: key, + StringValue: stringValue, + } + d.TimeSpan.Tags = append(d.TimeSpan.Tags, tag) + d.DB.Save(tag) + return d +} + +// DashboardWithDatabase wraps gorm.DB and provides helper methods +type DashboardWithDatabase struct { + User model.User + Dashboard model.Dashboard + t assert.TestingT + *gorm.DB +} diff --git a/test/db_test.go b/test/db_test.go new file mode 100644 index 0000000..5f8cdc8 --- /dev/null +++ b/test/db_test.go @@ -0,0 +1,103 @@ +package test_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +func TestInMemoryDB(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + assert.NotNil(t, db) +} + +func TestDatabase(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(1) + + user.AssertHasDevice("test device", false) + user.NewDevice(1, "lol", "test device") + user.AssertHasDevice("test device", true) + + ts := user.TimeSpan("2009-06-30T18:30:00Z", "2009-06-30T18:40:00Z") + + ts.AssertHasTagIgnoreValue("abc", false) + ts.AssertHasTag("abc", "def", false) + ts.Tag("abc", "def") + ts.AssertHasTag("abc", "def", true) + ts.AssertHasTagIgnoreValue("abc", true) + + db.NewUser(2, "abc", true) + db.NewUserPass(3, "xxx", []byte{5, 5}, true) + + expectedUsers := []model.User{ + {ID: 1, Name: "test1", Pass: []uint8{1, 2, 3}, Admin: false}, + {ID: 2, Name: "abc", Pass: []uint8{1, 2, 3}, Admin: true}, + {ID: 3, Name: "xxx", Pass: []uint8{5, 5}, Admin: true}} + + var users []model.User + db.Find(&users) + assert.Equal(t, expectedUsers, users) + + expectedDevices := []model.Device{{ + ID: 1, + Token: "lol", + Name: "test device", + UserID: 1, + Type: model.TypeNoExpiry, + ActiveAt: test.Time("2009-06-30T18:30:00Z"), + CreatedAt: test.Time("2009-06-30T18:30:00Z")}} + + var devices []model.Device + db.Find(&devices) + assert.Equal(t, expectedDevices, devices) + + expectedTimeSpans := []model.TimeSpan{{ + ID: 1, + UserID: 1, + StartUserTime: test.Time("2009-06-30T18:30:00Z"), + StartUTC: test.Time("2009-06-30T18:30:00Z"), + EndUserTime: test.TimeP("2009-06-30T18:40:00Z"), + EndUTC: test.TimeP("2009-06-30T18:40:00Z"), + Tags: []model.TimeSpanTag{{ + Key: "abc", + StringValue: "def", + TimeSpanID: 1, + }}, + }} + + var timeSpans []model.TimeSpan + db.Preload("Tags").Find(&timeSpans) + assert.Equal(t, expectedTimeSpans, timeSpans) + + ts.AssertExists(true) + db.Delete(new(model.TimeSpan), "id = ?", ts.TimeSpan.ID) + ts.AssertExists(false) + + user.AssertHasTagDefinition("oops", false) + user.NewTagDefinition("oops") + user.AssertHasTagDefinition("oops", true) + + dash := user.Dashboard("hello") + + dash.AssertHasRange("hello", false) + dash.Range("hello") + dash.AssertHasRange("hello", true) + + dash.AssertHasEntry("abc", false) + dash.Entry("abc") + dash.AssertHasEntry("abc", true) + + dash.AssertExists(true) + db.Delete(new(model.Dashboard), "id = ?", dash.Dashboard.ID) + dash.AssertExists(false) + + user.AssertExists(true) + db.Delete(new(model.User), "id = ?", user.User.ID) + user.AssertExists(false) + +} diff --git a/test/fake/device.go b/test/fake/device.go new file mode 100644 index 0000000..b7d3126 --- /dev/null +++ b/test/fake/device.go @@ -0,0 +1,13 @@ +package fake + +import ( + "context" + + "github.com/traggo/server/auth" + "github.com/traggo/server/model" +) + +// Device creates a context with a fake device. +func Device(device *model.Device) context.Context { + return auth.WithDevice(context.Background(), device) +} diff --git a/test/fake/user.go b/test/fake/user.go new file mode 100644 index 0000000..35f1f46 --- /dev/null +++ b/test/fake/user.go @@ -0,0 +1,22 @@ +package fake + +import ( + "context" + + "github.com/traggo/server/auth" + "github.com/traggo/server/model" +) + +// User create a context with a fake user. +func User(id int) context.Context { + return UserWithPerm(id, true) +} + +// UserWithPerm create a context with a fake user. +func UserWithPerm(id int, admin bool) context.Context { + return auth.WithUser(context.Background(), &model.User{ + ID: id, + Name: "fake", + Admin: admin, + }) +} diff --git a/test/fake_test.go b/test/fake_test.go new file mode 100644 index 0000000..e308360 --- /dev/null +++ b/test/fake_test.go @@ -0,0 +1,9 @@ +package test_test + +type fakeTesting struct { + hasErrors bool +} + +func (t *fakeTesting) Errorf(format string, args ...interface{}) { + t.hasErrors = true +} diff --git a/test/init.go b/test/init.go new file mode 100644 index 0000000..f8c88b0 --- /dev/null +++ b/test/init.go @@ -0,0 +1,10 @@ +package test + +import ( + "github.com/rs/zerolog" + "github.com/traggo/server/logger" +) + +func init() { + logger.Init(zerolog.WarnLevel) +} diff --git a/test/logger.go b/test/logger.go new file mode 100644 index 0000000..11e4383 --- /dev/null +++ b/test/logger.go @@ -0,0 +1,60 @@ +package test + +import ( + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "github.com/traggo/server/logger" +) + +// Logger the test logger with util methods +type Logger struct { + old zerolog.Logger + entries []Entry + t assert.TestingT +} + +// Entry a test logging entry +type Entry struct { + Level zerolog.Level + Message string +} + +// Dispose resets the logger +func (l *Logger) Dispose() { + log.Logger = l.old +} + +// NewLogger creates a new test logger +func NewLogger(t assert.TestingT) *Logger { + logger := &Logger{t: t} + log.Logger = zerolog.New(&noop{}).With().Timestamp().Logger().Hook(logger) + return logger +} + +// Run records log entries +func (l *Logger) Run(e *zerolog.Event, level zerolog.Level, message string) { + l.entries = append(l.entries, Entry{Level: level, Message: message}) +} + +// AssertCount asserts the amount of recorded logging entries +func (l *Logger) AssertCount(count int) { + assert.Len(l.t, l.entries, count) +} + +// AssertEntryExists asserts that a logging entry exists +func (l *Logger) AssertEntryExists(entry Entry) { + assert.Contains(l.t, l.entries, entry) +} + +type noop struct { +} + +func (*noop) Write(p []byte) (n int, err error) { + return len(p), nil +} + +// LogDebug enables debug log +func LogDebug() { + logger.Init(zerolog.DebugLevel) +} diff --git a/test/logger_test.go b/test/logger_test.go new file mode 100644 index 0000000..468481e --- /dev/null +++ b/test/logger_test.go @@ -0,0 +1,63 @@ +package test_test + +import ( + "testing" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "github.com/traggo/server/test" +) + +func TestLogger_AssertCount_Succeeds(t *testing.T) { + logger := test.NewLogger(t) + defer logger.Dispose() + + log.Error().Msg("error") + + logger.AssertCount(1) +} + +func TestLogger_AssertEntry_Succeeds(t *testing.T) { + logger := test.NewLogger(t) + defer logger.Dispose() + + log.Error().Msg("error") + + logger.AssertEntryExists(test.Entry{Message: "error", Level: zerolog.ErrorLevel}) +} + +func TestLogger_AssertCount_Fails(t *testing.T) { + fake := &fakeTesting{} + logger := test.NewLogger(fake) + defer logger.Dispose() + + log.Error().Msg("error") + + logger.AssertCount(2) + assert.True(t, fake.hasErrors) +} + +func TestLogger_AssertEntry_Fails_wrongLevel(t *testing.T) { + fake := &fakeTesting{} + logger := test.NewLogger(fake) + defer logger.Dispose() + + log.Error().Msg("error") + + logger.AssertEntryExists(test.Entry{Message: "error", Level: zerolog.InfoLevel}) + + assert.True(t, fake.hasErrors) +} + +func TestLogger_AssertEntry_Fails_wrongMessage(t *testing.T) { + fake := &fakeTesting{} + logger := test.NewLogger(fake) + defer logger.Dispose() + + log.Error().Msg("error") + + logger.AssertEntryExists(test.Entry{Message: "info", Level: zerolog.ErrorLevel}) + + assert.True(t, fake.hasErrors) +} diff --git a/test/time.go b/test/time.go new file mode 100644 index 0000000..e6246cf --- /dev/null +++ b/test/time.go @@ -0,0 +1,51 @@ +package test + +import ( + "time" + + "github.com/traggo/server/model" +) + +// Time parses a time panics if not valid +func Time(value string) time.Time { + parse, firstErr := time.ParseInLocation(time.RFC3339, value, time.UTC) + if firstErr != nil { + var err error + parse, err = time.ParseInLocation(time.RFC3339Nano, value, time.UTC) + if err != nil { + panic(firstErr) + } + } + return parse +} + +func timeWithCustomTZ(value string) time.Time { + parse, err := time.Parse(time.RFC3339, value) + if err != nil { + panic(err) + } + _, offset := parse.Zone() + return parse.In(time.FixedZone("unknown", offset)) +} + +// TimeP parses a time panics if not valid +func TimeP(value string) *time.Time { + t := Time(value) + return &t +} + +// ModelTimeUTC parses a model.Time in utc time. panics if not valid +func ModelTimeUTC(value string) model.Time { + return model.Time(Time(value)) +} + +// ModelTime parses a model.Time panics if not valid +func ModelTime(value string) model.Time { + return model.Time(timeWithCustomTZ(value)) +} + +// ModelTimeP parses a model.Time panics if not valid +func ModelTimeP(value string) *model.Time { + modelTime := ModelTime(value) + return &modelTime +} diff --git a/test/time_test.go b/test/time_test.go new file mode 100644 index 0000000..f6b5892 --- /dev/null +++ b/test/time_test.go @@ -0,0 +1,51 @@ +package test + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTime_panics(t *testing.T) { + assert.Panics(t, func() { + Time("asds") + }) +} + +func TestTime_succeeds(t *testing.T) { + Time("2009-06-30T18:30:00+02:00") +} + +func TestModelTime_panics(t *testing.T) { + assert.Panics(t, func() { + ModelTime("asds") + }) +} + +func TestModelTime_succeeds(t *testing.T) { + ModelTime("2009-06-30T18:30:00+02:00") +} + +func TestTimeP_panics(t *testing.T) { + assert.Panics(t, func() { + TimeP("asds") + }) +} + +func TestTimeP_succeeds(t *testing.T) { + TimeP("2009-06-30T18:30:00+02:00") +} + +func TestModelTimeP_panics(t *testing.T) { + assert.Panics(t, func() { + ModelTimeP("asds") + }) +} + +func TestModelTimeP_succeeds(t *testing.T) { + ModelTimeP("2009-06-30T18:30:00+02:00") +} + +func TestModelTimeUTC_succeeds(t *testing.T) { + ModelTimeUTC("2009-06-30T18:30:00+02:00") +} diff --git a/time/interval.go b/time/interval.go new file mode 100644 index 0000000..0f2c750 --- /dev/null +++ b/time/interval.go @@ -0,0 +1,46 @@ +package time + +import ( + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// InternalInterval converts gqlmodel to internal +func InternalInterval(interval gqlmodel.StatsInterval) model.Interval { + switch interval { + case gqlmodel.StatsIntervalHourly: + return model.IntervalHourly + case gqlmodel.StatsIntervalDaily: + return model.IntervalDaily + case gqlmodel.StatsIntervalWeekly: + return model.IntervalWeekly + case gqlmodel.StatsIntervalMonthly: + return model.IntervalMonthly + case gqlmodel.StatsIntervalYearly: + return model.IntervalYearly + case gqlmodel.StatsIntervalSingle: + return model.IntervalSingle + default: + panic("unknown interval type " + interval) + } +} + +// ExternalInterval converts internal to gqlmodel +func ExternalInterval(interval model.Interval) gqlmodel.StatsInterval { + switch interval { + case model.IntervalHourly: + return gqlmodel.StatsIntervalHourly + case model.IntervalDaily: + return gqlmodel.StatsIntervalDaily + case model.IntervalWeekly: + return gqlmodel.StatsIntervalWeekly + case model.IntervalMonthly: + return gqlmodel.StatsIntervalMonthly + case model.IntervalYearly: + return gqlmodel.StatsIntervalYearly + case model.IntervalSingle: + return gqlmodel.StatsIntervalSingle + default: + panic("unknown interval type " + interval) + } +} diff --git a/time/interval_test.go b/time/interval_test.go new file mode 100644 index 0000000..b23fc11 --- /dev/null +++ b/time/interval_test.go @@ -0,0 +1,23 @@ +package time + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +func TestInterval(t *testing.T) { + for _, inter := range []model.Interval{ + model.IntervalSingle, model.IntervalHourly, model.IntervalDaily, model.IntervalWeekly, model.IntervalMonthly, model.IntervalYearly, + } { + assert.Equal(t, inter, InternalInterval(ExternalInterval(inter))) + } + assert.Panics(t, func() { + InternalInterval(gqlmodel.StatsInterval("meh")) + }) + assert.Panics(t, func() { + ExternalInterval(model.Interval("meh")) + }) +} diff --git a/time/parse.go b/time/parse.go new file mode 100644 index 0000000..154c2c8 --- /dev/null +++ b/time/parse.go @@ -0,0 +1,53 @@ +package time + +import ( + "fmt" + "time" + + "github.com/jmattheis/go-timemath" + "github.com/traggo/server/model" +) + +const nowKey = "now" + +// RelativeRange represents a relative range. +type RelativeRange struct { + From string + To string +} + +// StaticRange represents a concrete range. +type StaticRange struct { + From time.Time + To time.Time +} + +// ParseRange parses a range and converts it to static ranges. +func ParseRange(now time.Time, r RelativeRange, interval model.Interval, startOf, endOf time.Weekday) ([]StaticRange, error) { + from, err := ParseTime(now, r.From, true, startOf) + if err != nil { + return nil, fmt.Errorf("range from: %s", err) + } + to, err := ParseTime(now, r.To, false, endOf) + if err != nil { + return nil, fmt.Errorf("range to: %s", err) + } + + return ranges(from, to, interval), nil +} + +// Validate tries to parse the input and only returns the error. +func Validate(value string) error { + _, err := ParseTime(time.Now(), value, true, time.Monday) + return err +} + +// ParseTime parses time. +func ParseTime(now time.Time, value string, startOf bool, weekday time.Weekday) (time.Time, error) { + parse, err := time.Parse(time.RFC3339, value) + if err == nil { + return parse, nil + } + + return timemath.Parse(now, value, startOf, weekday) +} diff --git a/time/parse_test.go b/time/parse_test.go new file mode 100644 index 0000000..07f8725 --- /dev/null +++ b/time/parse_test.go @@ -0,0 +1,224 @@ +package time + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +func TestParseRange(t *testing.T) { + + entries := []testEntry{ + now("2019-05-13T15:55:23Z"). + Range("now-1d", "now"). + Interval(model.IntervalSingle). + Expect(r("2019-05-12T15:55:23Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now-1d", "2019-05-13T18:52:23Z"). + Interval(model.IntervalSingle). + Expect(r("2019-05-12T15:55:23Z", "2019-05-13T18:52:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now-1w", "now"). + Interval(model.IntervalSingle). + Expect(r("2019-05-06T15:55:23Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now-10d", "now"). + Interval(model.IntervalSingle). + Expect(r("2019-05-03T15:55:23Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now-1M", "now"). + Interval(model.IntervalSingle). + Expect(r("2019-04-13T15:55:23Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now-1y", "now"). + Interval(model.IntervalSingle). + Expect(r("2018-05-13T15:55:23Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now-5s-3m-5h-2d-2w-3M-1y", "now"). + Interval(model.IntervalSingle). + Expect(r("2018-01-27T10:52:18Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now/s", "now"). + Interval(model.IntervalSingle). + Expect(r("2019-05-13T15:55:23Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now/m", "now"). + Interval(model.IntervalSingle). + Expect(r("2019-05-13T15:55:00Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now/h", "now"). + Interval(model.IntervalSingle). + Expect(r("2019-05-13T15:00:00Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now/d", "now"). + Interval(model.IntervalSingle). + Expect(r("2019-05-13T00:00:00Z", "2019-05-13T15:55:23Z")), + now("2019-05-15T15:55:23Z"). + Range("now/w", "now"). + Interval(model.IntervalSingle). + Expect(r("2019-05-13T00:00:00Z", "2019-05-15T15:55:23Z")), + now("2019-05-15T15:55:23Z"). + Range("now/M", "now"). + Interval(model.IntervalSingle). + Expect(r("2019-05-01T00:00:00Z", "2019-05-15T15:55:23Z")), + now("2019-05-15T15:55:23Z"). + Range("now/y", "now"). + Interval(model.IntervalSingle). + Expect(r("2019-01-01T00:00:00Z", "2019-05-15T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now", "now/s"). + Interval(model.IntervalSingle). + Expect(r("2019-05-13T15:55:23Z", "2019-05-13T15:55:23.999999999Z")), + now("2019-05-13T15:55:23Z"). + Range("now", "now/m"). + Interval(model.IntervalSingle). + Expect(r("2019-05-13T15:55:23Z", "2019-05-13T15:55:59.999999999Z")), + now("2019-05-13T15:55:23Z"). + Range("now", "now/h"). + Interval(model.IntervalSingle). + Expect(r("2019-05-13T15:55:23Z", "2019-05-13T15:59:59.999999999Z")), + now("2019-05-13T15:55:23Z"). + Range("now", "now/d"). + Interval(model.IntervalSingle). + Expect(r("2019-05-13T15:55:23Z", "2019-05-13T23:59:59.999999999Z")), + now("2019-05-13T15:55:23Z"). + Range("now", "now/d"). + Interval(model.IntervalSingle). + Expect(r("2019-05-13T15:55:23Z", "2019-05-13T23:59:59.999999999Z")), + now("2019-05-15T15:55:23Z"). + Range("now", "now/w"). + Interval(model.IntervalSingle). + Expect(r("2019-05-15T15:55:23Z", "2019-05-19T23:59:59.999999999Z")), + now("2019-05-15T15:55:23Z"). + Range("now", "now/M"). + Interval(model.IntervalSingle). + Expect(r("2019-05-15T15:55:23Z", "2019-05-31T23:59:59.999999999Z")), + now("2019-05-15T15:55:23Z"). + Range("now", "now/y"). + Interval(model.IntervalSingle). + Expect(r("2019-05-15T15:55:23Z", "2019-12-31T23:59:59.999999999Z")), + now("2019-05-15T15:55:23Z"). + Range("now-1y/y", "now-1y/y"). // last year + Interval(model.IntervalSingle). + Expect(r("2018-01-01T00:00:00Z", "2018-12-31T23:59:59.999999999Z")), + now("2019-05-15T15:55:23Z"). + Range("now-1w/w", "now-1w/w"). // last week + Interval(model.IntervalSingle). + Expect(r("2019-05-06T00:00:00Z", "2019-05-12T23:59:59.999999999Z")), + now("2019-05-15T15:55:23Z"). + Range("now-1w/w", "now-1w/w-2d"). // last work monday to friday + Interval(model.IntervalSingle). + Expect(r("2019-05-06T00:00:00Z", "2019-05-10T23:59:59.999999999Z")), + now("2019-05-15T15:55:23Z"). + Range("now-1w/w+5d", "now-1w/w"). // last weekend + Interval(model.IntervalSingle). + Expect(r("2019-05-11T00:00:00Z", "2019-05-12T23:59:59.999999999Z")), + now("2019-05-13T15:55:23Z"). + Range("now-1d", "now"). + Interval(model.IntervalYearly). + Expect(r("2019-05-12T15:55:23Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now-1d", "now"). + Interval(model.IntervalMonthly). + Expect(r("2019-05-12T15:55:23Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now-1d", "now"). + Interval(model.IntervalWeekly). + Expect(r("2019-05-12T15:55:23Z", "2019-05-13T15:55:23Z")), + now("2019-05-13T15:55:23Z"). + Range("now-5h/h", "now/h"). + Interval(model.IntervalHourly). + Expect( + r("2019-05-13T10:00:00Z", "2019-05-13T10:59:59Z"), + r("2019-05-13T11:00:00Z", "2019-05-13T11:59:59Z"), + r("2019-05-13T12:00:00Z", "2019-05-13T12:59:59Z"), + r("2019-05-13T13:00:00Z", "2019-05-13T13:59:59Z"), + r("2019-05-13T14:00:00Z", "2019-05-13T14:59:59Z"), + r("2019-05-13T15:00:00Z", "2019-05-13T15:59:59Z")), + now("2019-05-13T15:55:23Z"). + Range("now-5d/d", "now/d"). + Interval(model.IntervalDaily). + Expect( + r("2019-05-08T00:00:00Z", "2019-05-08T23:59:59Z"), + r("2019-05-09T00:00:00Z", "2019-05-09T23:59:59Z"), + r("2019-05-10T00:00:00Z", "2019-05-10T23:59:59Z"), + r("2019-05-11T00:00:00Z", "2019-05-11T23:59:59Z"), + r("2019-05-12T00:00:00Z", "2019-05-12T23:59:59Z"), + r("2019-05-13T00:00:00Z", "2019-05-13T23:59:59Z")), + now("2019-02-13T15:55:23Z"). + Range("now-5M/M", "now/M"). + Interval(model.IntervalMonthly). + Expect( + r("2018-09-01T00:00:00Z", "2018-09-30T23:59:59Z"), + r("2018-10-01T00:00:00Z", "2018-10-31T23:59:59Z"), + r("2018-11-01T00:00:00Z", "2018-11-30T23:59:59Z"), + r("2018-12-01T00:00:00Z", "2018-12-31T23:59:59Z"), + r("2019-01-01T00:00:00Z", "2019-01-31T23:59:59Z"), + r("2019-02-01T00:00:00Z", "2019-02-28T23:59:59Z")), + } + + for _, entry := range entries { + t.Run(fmt.Sprintf("now=%s;%s_to_%s;%s", entry.now, entry.from, entry.to, entry.interval), func(t *testing.T) { + result, err := ParseRange(entry.now, RelativeRange{From: entry.from, To: entry.to}, entry.interval, time.Monday, time.Sunday) + assert.NoError(t, err) + assert.Equal(t, entry.expectRanges, result) + }) + } +} + +func TestStuff(t *testing.T) { + assert.Panics(t, func() { + ranges(time.Time{}, time.Time{}, "meh") + }) + assert.NotNil(t, Validate("invalid")) + assert.Nil(t, Validate("now-1d")) + _, err := ParseRange(time.Now(), RelativeRange{ + From: "invalid", + To: "invalid", + }, model.IntervalDaily, time.Monday, time.Sunday) + assert.NotNil(t, err) + _, err = ParseRange(time.Now(), RelativeRange{ + From: "now-1d", + To: "invalid", + }, model.IntervalDaily, time.Monday, time.Sunday) + assert.NotNil(t, err) +} + +func now(now string) testEntry { + return testEntry{now: test.Time(now)} +} + +type testEntry struct { + now time.Time + from string + to string + interval model.Interval + expectRanges []StaticRange +} + +func r(from string, to string) StaticRange { + return StaticRange{ + From: test.Time(from), + To: test.Time(to), + } +} + +func (t testEntry) Expect(ranges ...StaticRange) testEntry { + t.expectRanges = ranges + return t +} + +func (t testEntry) Range(from string, to string) testEntry { + t.from = from + t.to = to + return t +} + +func (t testEntry) Interval(interval model.Interval) testEntry { + t.interval = interval + return t +} diff --git a/time/range.go b/time/range.go new file mode 100644 index 0000000..ed91d18 --- /dev/null +++ b/time/range.go @@ -0,0 +1,41 @@ +package time + +import ( + "time" + + "github.com/jmattheis/go-timemath" + "github.com/traggo/server/model" +) + +func ranges(from time.Time, to time.Time, interval model.Interval) []StaticRange { + switch interval { + case model.IntervalSingle: + return []StaticRange{{From: from, To: to}} + case model.IntervalHourly: + return rangeForUnit(from, to, timemath.Hour) + case model.IntervalDaily: + return rangeForUnit(from, to, timemath.Day) + case model.IntervalWeekly: + return rangeForUnit(from, to, timemath.Week) + case model.IntervalMonthly: + return rangeForUnit(from, to, timemath.Month) + case model.IntervalYearly: + return rangeForUnit(from, to, timemath.Year) + default: + panic("unknown interval type") + } +} + +func rangeForUnit(from time.Time, to time.Time, u timemath.Unit) []StaticRange { + var result []StaticRange + newFrom := from + for newFrom.Before(to) { + newTo := timemath.Second.Subtract(u.Add(newFrom, 1), 1) + if newTo.After(to) { + newTo = to + } + result = append(result, StaticRange{From: newFrom, To: newTo}) + newFrom = timemath.Second.Add(newTo, 1) + } + return result +} diff --git a/timespan/convert.go b/timespan/convert.go new file mode 100644 index 0000000..0bb5fd7 --- /dev/null +++ b/timespan/convert.go @@ -0,0 +1,89 @@ +package timespan + +import ( + "fmt" + "time" + + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +func timespanToInternal(userID int, start model.Time, end *model.Time, tags []*gqlmodel.InputTimeSpanTag, note string) (model.TimeSpan, error) { + _, offset := start.Time().Zone() + span := model.TimeSpan{ + StartUserTime: start.OmitTimeZone(), + StartUTC: start.UTC(), + UserID: userID, + Tags: tagsToInternal(tags), + OffsetUTC: offset, + Note: note, + } + + if end != nil { + if start.Time().After(end.Time()) { + return span, fmt.Errorf("start must be before end") + } + endUser := end.OmitTimeZone() + span.EndUserTime = &endUser + endUTC := end.UTC() + span.EndUTC = &endUTC + } + + return span, nil +} + +func timeSpanToExternal(span model.TimeSpan) *gqlmodel.TimeSpan { + location := time.FixedZone("unknown", span.OffsetUTC) + + result := gqlmodel.TimeSpan{ + Start: model.Time(span.StartUTC.In(location)), + End: nil, + ID: span.ID, + Tags: tagsToExternal(span.Tags), + Note: span.Note, + } + if span.EndUTC != nil && !span.EndUTC.IsZero() { + end := *span.EndUTC + endModel := model.Time(end.In(location)) + result.End = &endModel + } + + return &result +} + +func tagsToExternal(tags []model.TimeSpanTag) []*gqlmodel.TimeSpanTag { + result := []*gqlmodel.TimeSpanTag{} + for _, tag := range tags { + result = append(result, &gqlmodel.TimeSpanTag{ + Key: tag.Key, + Value: tag.StringValue, + }) + } + return result +} + +func tagsToInternal(gqls []*gqlmodel.InputTimeSpanTag) []model.TimeSpanTag { + result := make([]model.TimeSpanTag, 0) + for _, tag := range gqls { + result = append(result, tagToInternal(*tag)) + } + return result +} + +func tagToInternal(gqls gqlmodel.InputTimeSpanTag) model.TimeSpanTag { + return model.TimeSpanTag{ + Key: gqls.Key, + StringValue: gqls.Value, + } +} + +func tagsToInputTag(tags []model.TimeSpanTag) []*gqlmodel.InputTimeSpanTag { + result := make([]*gqlmodel.InputTimeSpanTag, 0) + for _, tag := range tags { + result = append(result, &gqlmodel.InputTimeSpanTag{ + Key: tag.Key, + Value: tag.StringValue, + }) + } + return result +} diff --git a/timespan/copy.go b/timespan/copy.go new file mode 100644 index 0000000..abe691b --- /dev/null +++ b/timespan/copy.go @@ -0,0 +1,21 @@ +package timespan + +import ( + "context" + "fmt" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// CopyTimeSpan copies a time span. +func (r *ResolverForTimeSpan) CopyTimeSpan(ctx context.Context, id int, start model.Time, end *model.Time) (*gqlmodel.TimeSpan, error) { + old := &model.TimeSpan{ID: id} + + if r.DB.Preload("Tags").Where("user_id = ?", auth.GetUser(ctx).ID).Find(old).RecordNotFound() { + return nil, fmt.Errorf("time span with id %d does not exist", id) + } + + return r.CreateTimeSpan(ctx, start, end, tagsToInputTag(old.Tags), old.Note) +} diff --git a/timespan/copy_test.go b/timespan/copy_test.go new file mode 100644 index 0000000..eb63318 --- /dev/null +++ b/timespan/copy_test.go @@ -0,0 +1,103 @@ +package timespan + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestCopy_noPermission(t *testing.T) { + db := test.InMemoryDB(t) + db.User(3) + db.User(5) + db.User(2) + db.Create(&model.TimeSpan{ + ID: 3, + UserID: 3, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T18:30:00Z"), + EndUTC: test.TimeP("2019-06-10T16:30:00Z"), + Tags: []model.TimeSpanTag{}, + }) + defer db.Close() + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.CopyTimeSpan(fake.User(2), 3, test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T18:35:00+02:00")) + require.Nil(t, timeSpan) + require.EqualError(t, err, "time span with id 3 does not exist") + assertTimeSpanCount(t, db, 1) +} + +func TestCopy(t *testing.T) { + db := test.InMemoryDB(t) + db.User(3) + db.Create(&model.TagDefinition{Key: "test", UserID: 3}) + db.Create(&model.TimeSpan{ + ID: 3, + UserID: 3, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T18:30:00Z"), + EndUTC: test.TimeP("2019-06-10T16:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{ + {Key: "test", TimeSpanID: 3}, + }, + Note: "A special note", + }) + defer db.Close() + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.CopyTimeSpan(fake.User(3), 3, test.ModelTime("2019-06-15T10:30:00+02:00"), nil) + require.NoError(t, err) + + expected := &gqlmodel.TimeSpan{ + ID: 4, + Start: test.ModelTime("2019-06-15T10:30:00+02:00"), + End: nil, + Tags: []*gqlmodel.TimeSpanTag{ + {Key: "test"}, + }, + Note: "A special note", + } + + require.Equal(t, expected, timeSpan) + + assertTimeSpanCount(t, db, 2) + assertTimeSpanExist(t, db, model.TimeSpan{ + ID: 4, + UserID: 3, + StartUserTime: test.Time("2019-06-15T10:30:00Z"), + StartUTC: test.Time("2019-06-15T08:30:00Z"), + EndUserTime: nil, + EndUTC: nil, + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{ + {Key: "test", TimeSpanID: 4}, + }, + Note: "A special note", + }) +} + +func TestCopy_notFound(t *testing.T) { + db := test.InMemoryDB(t) + db.User(3) + db.User(2) + db.User(5) + defer db.Close() + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.CopyTimeSpan(fake.User(2), 3, test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T18:35:00+02:00")) + require.Nil(t, timeSpan) + require.EqualError(t, err, "time span with id 3 does not exist") + assertTimeSpanCount(t, db, 0) +} diff --git a/timespan/create.go b/timespan/create.go new file mode 100644 index 0000000..64b3bbb --- /dev/null +++ b/timespan/create.go @@ -0,0 +1,26 @@ +package timespan + +import ( + "context" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// CreateTimeSpan creates a time span +func (r *ResolverForTimeSpan) CreateTimeSpan(ctx context.Context, start model.Time, end *model.Time, tags []*gqlmodel.InputTimeSpanTag, note string) (*gqlmodel.TimeSpan, error) { + timeSpan, err := timespanToInternal(auth.GetUser(ctx).ID, start, end, tags, note) + if err != nil { + return nil, err + } + + if err := tagsExist(r.DB, auth.GetUser(ctx).ID, timeSpan.Tags); err != nil { + return nil, err + } + + r.DB.Create(&timeSpan) + + external := timeSpanToExternal(timeSpan) + return external, nil +} diff --git a/timespan/create_test.go b/timespan/create_test.go new file mode 100644 index 0000000..55d3bbd --- /dev/null +++ b/timespan/create_test.go @@ -0,0 +1,138 @@ +package timespan + +import ( + "testing" + + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func Test_Create_withoutEnd(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.CreateTimeSpan(fake.User(5), test.ModelTime("2019-06-10T18:30:00+02:00"), nil, nil, "") + + require.Nil(t, err) + expected := &gqlmodel.TimeSpan{ + ID: 1, + Start: test.ModelTime("2019-06-10T18:30:00+02:00"), + Tags: []*gqlmodel.TimeSpanTag{}, + Note: "", + } + require.Equal(t, expected, timeSpan) + assertTimeSpanCount(t, db, 1) + assertTimeSpanExist(t, db, model.TimeSpan{ + ID: 1, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + Note: "", + }) +} + +func Test_Create(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + + resolver := ResolverForTimeSpan{DB: db.DB} + note := "A note describing the timespan" + timeSpan, err := resolver.CreateTimeSpan(fake.User(5), test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T19:30:00+02:00"), nil, note) + log.Debug().Msg("oops") + require.Nil(t, err) + expected := &gqlmodel.TimeSpan{ + ID: 1, + Start: test.ModelTime("2019-06-10T18:30:00+02:00"), + End: test.ModelTimeP("2019-06-10T19:30:00+02:00"), + Tags: []*gqlmodel.TimeSpanTag{}, + Note: note, + } + require.Equal(t, expected, timeSpan) + assertTimeSpanCount(t, db, 1) + assertTimeSpanExist(t, db, model.TimeSpan{ + ID: 1, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T19:30:00Z"), + EndUTC: test.TimeP("2019-06-10T17:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + Note: note, + }) +} + +func Test_Create_fail_endBeforeStart(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.CreateTimeSpan(fake.User(5), test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T17:30:00+02:00"), nil, "") + require.Nil(t, timeSpan) + require.EqualError(t, err, "start must be before end") + assertTimeSpanCount(t, db, 0) +} + +func Test_Create_fail_notExistingTag(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.CreateTimeSpan(fake.User(5), test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T18:35:00+02:00"), []*gqlmodel.InputTimeSpanTag{{Key: "test"}}, "") + require.Nil(t, timeSpan) + require.EqualError(t, err, "tag 'test' does not exist") + assertTimeSpanCount(t, db, 0) +} + +func Test_Create_withTag(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.CreateTimeSpan(fake.User(5), test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T19:30:00+02:00"), []*gqlmodel.InputTimeSpanTag{{Key: "test"}}, "") + require.NotNil(t, timeSpan) + require.NoError(t, err) + assertTimeSpanCount(t, db, 1) + assertTimeSpanExist(t, db, model.TimeSpan{ + ID: 1, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T19:30:00Z"), + EndUTC: test.TimeP("2019-06-10T17:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{ + {Key: "test", TimeSpanID: 1}, + }, + Note: "", + }) +} + +func Test_Create_fail_tagAddedMultipleTimes(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.CreateTimeSpan(fake.User(5), test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T18:35:00+02:00"), []*gqlmodel.InputTimeSpanTag{{Key: "test"}, {Key: "test"}}, "") + require.Nil(t, timeSpan) + require.EqualError(t, err, "tag 'test' is present multiple times") + assertTimeSpanCount(t, db, 0) +} diff --git a/timespan/get.go b/timespan/get.go new file mode 100644 index 0000000..98ead97 --- /dev/null +++ b/timespan/get.go @@ -0,0 +1,76 @@ +package timespan + +import ( + "context" + "errors" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// TimeSpans returns all time spans for a user +func (r *ResolverForTimeSpan) TimeSpans(ctx context.Context, fromInclusive *model.Time, toInclusive *model.Time, cursor *gqlmodel.InputCursor) (*gqlmodel.PagedTimeSpans, error) { + user := auth.GetUser(ctx) + cursor = normalize(cursor) + + if cursor.StartID == nil { + var s model.TimeSpan + if err := r.DB.Model(new(model.TimeSpan)).Select("max(id) as id").Find(&s).Error; err != nil { + return nil, err + } + cursor.StartID = &s.ID + } + + call := r.DB.Preload("Tags").Where("user_id = ?", user.ID).Not("end_user_time is NULL").Order("start_user_time DESC").Limit(*cursor.PageSize) + if cursor.Offset != nil && cursor.StartID != nil { + call = call.Where("id <= ?", *cursor.StartID).Offset(*cursor.Offset) + } + if fromInclusive != nil { + if toInclusive != nil { + if fromInclusive.Time().After(toInclusive.Time()) { + return nil, errors.New("fromInclusive must be before toInclusive") + } + + call = call.Where("start_user_time <= ? AND end_user_time >= ?", toInclusive.OmitTimeZone(), fromInclusive.OmitTimeZone()) + } else { + call = call.Where("start_user_time >= ? OR end_user_time >= ?", fromInclusive.OmitTimeZone(), fromInclusive.OmitTimeZone()) + } + } else if toInclusive != nil { + call = call.Where("end_user_time <= ? OR start_user_time <= ?", toInclusive.OmitTimeZone(), toInclusive.OmitTimeZone()) + } + + var timeSpans []model.TimeSpan + call.Find(&timeSpans) + + var result []*gqlmodel.TimeSpan + for _, span := range timeSpans { + result = append(result, timeSpanToExternal(span)) + } + return &gqlmodel.PagedTimeSpans{ + TimeSpans: result, + Cursor: &gqlmodel.Cursor{ + HasMore: len(timeSpans) != 0 && *cursor.Offset%*cursor.PageSize == 0, + Offset: *cursor.Offset + len(timeSpans), + StartID: *cursor.StartID, + PageSize: *cursor.PageSize}, + }, nil +} + +func normalize(cursor *gqlmodel.InputCursor) *gqlmodel.InputCursor { + if cursor == nil { + cursor = &gqlmodel.InputCursor{} + } + + maxPageSize := 100 + if cursor.PageSize == nil || maxPageSize < *cursor.PageSize { + cursor.PageSize = &maxPageSize + } + + if cursor.Offset == nil { + zero := 0 + cursor.Offset = &zero + } + + return cursor +} diff --git a/timespan/get_test.go b/timespan/get_test.go new file mode 100644 index 0000000..8be7d17 --- /dev/null +++ b/timespan/get_test.go @@ -0,0 +1,275 @@ +package timespan + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +var ( + timeSpan1 = &model.TimeSpan{ + ID: 1, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T18:40:00Z"), + StartUTC: test.Time("2019-06-10T18:30:00Z"), + EndUTC: test.TimeP("2019-06-10T18:40:00Z"), + OffsetUTC: 0, + Tags: []model.TimeSpanTag{ + {Key: "test"}, + {Key: "test2"}, + }, + } + modelTimeSpan1 = gqlmodel.TimeSpan{ + ID: 1, + Start: test.ModelTime("2019-06-10T18:30:00Z"), + End: test.ModelTimeP("2019-06-10T18:40:00Z"), + Tags: []*gqlmodel.TimeSpanTag{ + {Key: "test"}, + {Key: "test2"}, + }, + } + timeSpan2 = &model.TimeSpan{ + ID: 2, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:40:00Z"), + EndUserTime: test.TimeP("2019-06-10T18:50:00Z"), + StartUTC: test.Time("2019-06-10T18:40:00Z"), + EndUTC: test.TimeP("2019-06-10T18:50:00Z"), + OffsetUTC: 0, + Tags: []model.TimeSpanTag{ + {Key: "test"}, + {Key: "test2"}, + }, + } + modelTimeSpan2 = gqlmodel.TimeSpan{ + ID: 2, + Start: test.ModelTime("2019-06-10T18:40:00Z"), + End: test.ModelTimeP("2019-06-10T18:50:00Z"), + Tags: []*gqlmodel.TimeSpanTag{ + {Key: "test"}, + {Key: "test2"}, + }, + } + timeSpan3 = &model.TimeSpan{ + ID: 3, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:50:00Z"), + EndUserTime: test.TimeP("2019-06-10T19:00:00Z"), + StartUTC: test.Time("2019-06-10T18:50:00Z"), + EndUTC: test.TimeP("2019-06-10T19:00:00Z"), + OffsetUTC: 0, + Tags: []model.TimeSpanTag{ + {Key: "test"}, + {Key: "test2"}, + }, + } + modelTimeSpan3 = gqlmodel.TimeSpan{ + ID: 3, + Start: test.ModelTime("2019-06-10T18:50:00Z"), + End: test.ModelTimeP("2019-06-10T19:00:00Z"), + Tags: []*gqlmodel.TimeSpanTag{ + {Key: "test"}, + {Key: "test2"}, + }, + } + timeSpan4 = &model.TimeSpan{ + ID: 4, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:00:00Z"), + EndUserTime: test.TimeP("2019-06-10T19:00:00Z"), + StartUTC: test.Time("2019-06-10T18:00:00Z"), + EndUTC: test.TimeP("2019-06-10T19:00:00Z"), + OffsetUTC: 0, + Tags: []model.TimeSpanTag{ + {Key: "test"}, + {Key: "test2"}, + }, + } + modelTimeSpan4 = gqlmodel.TimeSpan{ + ID: 4, + Start: test.ModelTime("2019-06-10T18:00:00Z"), + End: test.ModelTimeP("2019-06-10T19:00:00Z"), + Tags: []*gqlmodel.TimeSpanTag{ + {Key: "test"}, + {Key: "test2"}, + }, + } + runningTimeSpan = &model.TimeSpan{ + ID: 5, + UserID: 5, + StartUserTime: test.Time("2019-06-11T18:00:00Z"), + StartUTC: test.Time("2019-06-11T18:00:00Z"), + EndUserTime: nil, + EndUTC: nil, + OffsetUTC: 0, + Tags: []model.TimeSpanTag{ + {Key: "test"}, + {Key: "test2"}, + }, + } + modelRunningTimeSpan = gqlmodel.TimeSpan{ + ID: 5, + Start: test.ModelTime("2019-06-11T18:00:00Z"), + End: nil, + Tags: []*gqlmodel.TimeSpanTag{ + {Key: "test"}, + {Key: "test2"}, + }, + } + timeSpanOtherUser = &model.TimeSpan{ + ID: 6, + UserID: 2, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T18:40:00Z"), + StartUTC: test.Time("2019-06-10T18:30:00Z"), + EndUTC: test.TimeP("2019-06-10T18:40:00Z"), + OffsetUTC: 0, + Tags: []model.TimeSpanTag{ + {Key: "test"}, + {Key: "test2"}, + }, + } +) + +type data struct { + DB []*model.TimeSpan + From *model.Time + To *model.Time + Expected []*gqlmodel.TimeSpan + Cursor *gqlmodel.InputCursor +} + +func (d data) String() string { + str := "DB=[" + for _, v := range d.DB { + str += fmt.Sprint(v.ID) + "," + } + str = str + "]" + if d.From != nil { + str = str + "|From=" + d.From.Time().Format(time.RFC3339) + } + if d.To != nil { + str = str + "|To=" + d.To.Time().Format(time.RFC3339) + } + + str += "|Expected=[" + for _, v := range d.Expected { + str += fmt.Sprint(v.ID) + "," + } + str = str + "]" + return str +} + +func TestGet(t *testing.T) { + d := []data{ + { + DB: []*model.TimeSpan{timeSpan1, timeSpanOtherUser}, + From: nil, + To: nil, + Expected: []*gqlmodel.TimeSpan{&modelTimeSpan1}, + }, + { + DB: []*model.TimeSpan{timeSpan1, timeSpan2, timeSpan3, timeSpan4, runningTimeSpan}, + From: nil, + To: nil, + Expected: []*gqlmodel.TimeSpan{&modelTimeSpan3, &modelTimeSpan2, &modelTimeSpan1, &modelTimeSpan4}, + }, + { + DB: []*model.TimeSpan{timeSpan1, timeSpan2, timeSpan3, timeSpan4}, + From: nil, + To: nil, + Cursor: &gqlmodel.InputCursor{PageSize: p(1)}, + Expected: []*gqlmodel.TimeSpan{&modelTimeSpan3}, + }, + { + DB: []*model.TimeSpan{timeSpan1, timeSpan2, timeSpan3, timeSpan4}, + From: nil, + To: nil, + Cursor: &gqlmodel.InputCursor{PageSize: p(1), Offset: p(1), StartID: p(4)}, + Expected: []*gqlmodel.TimeSpan{&modelTimeSpan2}, + }, + { + DB: []*model.TimeSpan{timeSpan1, timeSpan2, timeSpan3, timeSpan4}, + From: nil, + To: nil, + Cursor: &gqlmodel.InputCursor{PageSize: p(1), Offset: p(0), StartID: p(1)}, + Expected: []*gqlmodel.TimeSpan{&modelTimeSpan1}, + }, + { + DB: []*model.TimeSpan{timeSpan1, timeSpan2, timeSpan3, timeSpan4}, + From: test.ModelTimeP("2019-06-10T18:40:01+02:00"), + To: nil, + Expected: []*gqlmodel.TimeSpan{&modelTimeSpan3, &modelTimeSpan2, &modelTimeSpan4}, + }, + { + DB: []*model.TimeSpan{timeSpan1, timeSpan2, timeSpan3, timeSpan4}, + From: modelTimeSpan1.End, + To: nil, + Expected: []*gqlmodel.TimeSpan{&modelTimeSpan3, &modelTimeSpan2, &modelTimeSpan1, &modelTimeSpan4}, + }, + { + DB: []*model.TimeSpan{timeSpan1, timeSpan2, timeSpan3, timeSpan4}, + From: nil, + To: test.ModelTimeP("2019-06-10T18:45:00+02:00"), + Expected: []*gqlmodel.TimeSpan{&modelTimeSpan2, &modelTimeSpan1, &modelTimeSpan4}, + }, + { + DB: []*model.TimeSpan{timeSpan1, timeSpan2, timeSpan3, timeSpan4}, + From: test.ModelTimeP("2019-06-10T18:35:00+02:00"), + To: test.ModelTimeP("2019-06-10T18:45:00+02:00"), + Expected: []*gqlmodel.TimeSpan{&modelTimeSpan2, &modelTimeSpan1, &modelTimeSpan4}, + }, + { + DB: []*model.TimeSpan{timeSpan1, timeSpan2, timeSpan3, timeSpan4}, + From: test.ModelTimeP("2019-06-10T18:55:00+02:00"), + To: test.ModelTimeP("2019-06-10T18:56:00+02:00"), + Expected: []*gqlmodel.TimeSpan{&modelTimeSpan3, &modelTimeSpan4}, + }, + { + DB: []*model.TimeSpan{runningTimeSpan}, + From: test.ModelTimeP("2019-06-11T18:30:00Z"), + To: test.ModelTimeP("2019-06-11T19:00:00Z"), + Expected: nil, + }, + } + + for i, testData := range d { + t.Run(fmt.Sprintf("%d>%s", i, testData), func(t *testing.T) { + db := test.InMemoryDB(t) + db.User(5) + db.User(2) + defer db.Close() + for _, entry := range testData.DB { + db.Create(entry) + } + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpans, err := resolver.TimeSpans(fake.User(5), testData.From, testData.To, testData.Cursor) + + require.NoError(t, err) + require.Equal(t, testData.Expected, timeSpans.TimeSpans) + }) + } +} + +func TestGet_fail_toBeforeStart(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpans, err := resolver.TimeSpans(fake.User(5), test.ModelTimeP("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T17:30:00+02:00"), nil) + require.Nil(t, timeSpans) + require.EqualError(t, err, "fromInclusive must be before toInclusive") +} + +func p(i int) *int { + return &i +} diff --git a/timespan/remove.go b/timespan/remove.go new file mode 100644 index 0000000..d96f888 --- /dev/null +++ b/timespan/remove.go @@ -0,0 +1,23 @@ +package timespan + +import ( + "context" + "fmt" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// RemoveTimeSpan removes a timespan. +func (r *ResolverForTimeSpan) RemoveTimeSpan(ctx context.Context, id int) (*gqlmodel.TimeSpan, error) { + timeSpan := model.TimeSpan{ID: id} + if r.DB.Preload("Tags").Where("user_id = ?", auth.GetUser(ctx).ID).Find(&timeSpan).RecordNotFound() { + return nil, fmt.Errorf("timespan with id %d does not exist", timeSpan.ID) + } + + remove := r.DB.Where(&model.TimeSpan{ID: id}).Delete(new(model.TimeSpan)) + + external := timeSpanToExternal(timeSpan) + return external, remove.Error +} diff --git a/timespan/remove_test.go b/timespan/remove_test.go new file mode 100644 index 0000000..fab58d8 --- /dev/null +++ b/timespan/remove_test.go @@ -0,0 +1,78 @@ +package timespan + +import ( + "testing" + + "github.com/traggo/server/generated/gqlmodel" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestRemoveTimeSpan_succeeds_removesTimeSpan(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(3) + ts := user.TimeSpan("2019-06-11T18:00:00Z", "2019-06-11T18:00:00Z") + ts.Tag("hello", "world") + + resolver := ResolverForTimeSpan{DB: db.DB} + actual, err := resolver.RemoveTimeSpan(fake.User(3), ts.TimeSpan.ID) + require.NoError(t, err) + expected := &gqlmodel.TimeSpan{ + ID: ts.TimeSpan.ID, + Start: test.ModelTime("2019-06-11T18:00:00Z"), + End: test.ModelTimeP("2019-06-11T18:00:00Z"), + Tags: []*gqlmodel.TimeSpanTag{ + {Key: "hello", Value: "world"}, + }, + } + require.Equal(t, expected, actual) + assertTimeSpanCount(t, db, 0) +} + +func TestRemoveTimeSpan_succeeds_removesTags(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(3) + ts := user.TimeSpan("2019-06-11T18:00:00Z", "2019-06-11T18:00:00Z") + ts.Tag("hello", "world") + + resolver := ResolverForTimeSpan{DB: db.DB} + _, err := resolver.RemoveTimeSpan(fake.User(3), ts.TimeSpan.ID) + require.NoError(t, err) + + ts.AssertHasTag("hello", "world", false) +} + +func TestRemoveTimeSpan_fails_notExistingTimeSpan(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(3) + + resolver := ResolverForTimeSpan{DB: db.DB} + _, err := resolver.RemoveTimeSpan(fake.User(3), 5) + require.EqualError(t, err, "timespan with id 5 does not exist") +} + +func TestRemoveTimeSpan_fails_noPermission(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(3) + db.User(5) + db.Create(&model.TimeSpan{ + StartUserTime: test.Time("2019-06-11T18:00:00Z"), + StartUTC: test.Time("2019-06-11T18:00:00Z"), + EndUserTime: nil, + EndUTC: nil, + OffsetUTC: 0, + ID: 1, + UserID: 3, + }) + + resolver := ResolverForTimeSpan{DB: db.DB} + _, err := resolver.RemoveTimeSpan(fake.User(5), 1) + require.EqualError(t, err, "timespan with id 1 does not exist") +} diff --git a/timespan/replace_tags.go b/timespan/replace_tags.go new file mode 100644 index 0000000..c89a64b --- /dev/null +++ b/timespan/replace_tags.go @@ -0,0 +1,71 @@ +package timespan + +import ( + "context" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// ReplaceTimeSpanTags replaces time span tags. +func (r *ResolverForTimeSpan) ReplaceTimeSpanTags(ctx context.Context, fromExternal gqlmodel.InputTimeSpanTag, toExternal gqlmodel.InputTimeSpanTag, opt gqlmodel.InputReplaceOptions) (*bool, error) { + userID := auth.GetUser(ctx).ID + + from := tagToInternal(fromExternal) + to := tagToInternal(toExternal) + + if err := tagsExist(r.DB, userID, []model.TimeSpanTag{from}); err != nil { + return nil, err + } + if err := tagsExist(r.DB, userID, []model.TimeSpanTag{to}); err != nil { + return nil, err + } + + tx := r.DB.Begin() + + hasToKey := tx.Table("time_span_tags as innertst"). + Where("innertst.key = ?", to.Key). + Where("innerts.id = innertst.time_span_id"). + SubQuery() + + if opt.Override == gqlmodel.OverrideModeOverride { + timeSpanIdsWithExistingToKey := tx.Table("time_spans as innerts"). + Select("id"). + Where("innerts.user_id = ?", userID). + Where("EXISTS ?", hasToKey). + SubQuery() + + if err := tx.Where("time_span_id in ?", timeSpanIdsWithExistingToKey). + Where(&model.TimeSpanTag{Key: to.Key}). + Delete(new(model.TimeSpanTag)).Error; err != nil { + tx.Rollback() + return nil, err + } + } + + timeSpansIdsOfUser := tx.Table("time_spans as innerts"). + Select("id"). + Where("innerts.user_id = ?", userID). + Where("NOT(EXISTS ?)", hasToKey). + SubQuery() + + if update := tx.Model(&model.TimeSpanTag{}). + Where("time_span_id in ?", timeSpansIdsOfUser). + Where(from). + Updates(to); update.Error != nil { + tx.Rollback() + return nil, update.Error + } + + if opt.Override == gqlmodel.OverrideModeDiscard { + if err := tx.Where(&from).Delete(new(model.TimeSpanTag)).Error; err != nil { + tx.Rollback() + return nil, err + } + } + + commit := tx.Commit() + + return nil, commit.Error +} diff --git a/timespan/replace_tags_test.go b/timespan/replace_tags_test.go new file mode 100644 index 0000000..093923c --- /dev/null +++ b/timespan/replace_tags_test.go @@ -0,0 +1,109 @@ +package timespan + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestReplace_simple(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(1) + user.NewTagDefinition("old") + user.NewTagDefinition("new") + ts := user.TimeSpan("2019-06-11T18:00:00Z", "2019-06-11T18:00:00Z") + ts.Tag("old", "1") + + resolver := ResolverForTimeSpan{DB: db.DB} + + _, err := resolver.ReplaceTimeSpanTags(fake.User(1), + gqlmodel.InputTimeSpanTag{Key: "old", Value: "1"}, + gqlmodel.InputTimeSpanTag{Key: "new", Value: "1.0"}, + gqlmodel.InputReplaceOptions{Override: gqlmodel.OverrideModeDiscard}) + require.NoError(t, err) + + ts.AssertHasTag("new", "1.0", true) + ts.AssertHasTag("old", "1", false) +} + +func TestReplace_discard(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(1) + user.NewTagDefinition("old") + user.NewTagDefinition("new") + ts := user.TimeSpan("2019-06-11T18:00:00Z", "2019-06-11T18:00:00Z") + ts.Tag("old", "1") + ts.Tag("new", "2.0") + + resolver := ResolverForTimeSpan{DB: db.DB} + + _, err := resolver.ReplaceTimeSpanTags(fake.User(1), + gqlmodel.InputTimeSpanTag{Key: "old", Value: "1"}, + gqlmodel.InputTimeSpanTag{Key: "new", Value: "1.0"}, + gqlmodel.InputReplaceOptions{Override: gqlmodel.OverrideModeDiscard}) + require.NoError(t, err) + + ts.AssertHasTag("new", "2.0", true) + ts.AssertHasTag("new", "1.0", false) + ts.AssertHasTagIgnoreValue("old", false) +} + +func TestReplace_override(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(1) + user.NewTagDefinition("old") + user.NewTagDefinition("new") + ts := user.TimeSpan("2019-06-11T18:00:00Z", "2019-06-11T18:00:00Z") + ts.Tag("old", "1") + ts.Tag("new", "2.0") + + resolver := ResolverForTimeSpan{DB: db.DB} + + _, err := resolver.ReplaceTimeSpanTags(fake.User(1), + gqlmodel.InputTimeSpanTag{Key: "old", Value: "1"}, + gqlmodel.InputTimeSpanTag{Key: "new", Value: "1.0"}, + gqlmodel.InputReplaceOptions{Override: gqlmodel.OverrideModeOverride}) + require.NoError(t, err) + + ts.AssertHasTag("new", "2.0", false) + ts.AssertHasTag("new", "1.0", true) + ts.AssertHasTagIgnoreValue("old", false) +} + +func TestReplace_multiuser(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + left := db.User(1) + right := db.User(2) + + left.NewTagDefinition("old") + left.NewTagDefinition("new") + right.NewTagDefinition("old") + right.NewTagDefinition("new") + leftTs := left.TimeSpan("2019-06-11T18:00:00Z", "2019-06-11T18:00:00Z") + leftTs.Tag("old", "1") + leftTs.Tag("new", "2.0") + rightTs := right.TimeSpan("2019-06-11T18:00:00Z", "2019-06-11T18:00:00Z") + rightTs.Tag("old", "1") + rightTs.Tag("new", "2.0") + + resolver := ResolverForTimeSpan{DB: db.DB} + + _, err := resolver.ReplaceTimeSpanTags(fake.User(1), + gqlmodel.InputTimeSpanTag{Key: "old", Value: "1"}, + gqlmodel.InputTimeSpanTag{Key: "new", Value: "1.0"}, + gqlmodel.InputReplaceOptions{Override: gqlmodel.OverrideModeOverride}) + require.NoError(t, err) + + leftTs.AssertHasTag("new", "2.0", false) + leftTs.AssertHasTag("new", "1.0", true) + leftTs.AssertHasTagIgnoreValue("old", false) + rightTs.AssertHasTag("old", "1", true) + rightTs.AssertHasTag("new", "2.0", true) +} diff --git a/timespan/stop.go b/timespan/stop.go new file mode 100644 index 0000000..c433dd3 --- /dev/null +++ b/timespan/stop.go @@ -0,0 +1,33 @@ +package timespan + +import ( + "context" + "fmt" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// StopTimeSpan sets an end date to an existing time span. +func (r *ResolverForTimeSpan) StopTimeSpan(ctx context.Context, id int, end model.Time) (*gqlmodel.TimeSpan, error) { + old := &model.TimeSpan{ID: id} + + if r.DB.Preload("Tags").Where("user_id = ?", auth.GetUser(ctx).ID).Find(old).RecordNotFound() { + return nil, fmt.Errorf("time span with id %d does not exist", id) + } + + if old.EndUTC != nil { + return nil, fmt.Errorf("timespan with id %d has already an end date", id) + } + + utc := end.UTC() + old.EndUTC = &utc + userTime := end.OmitTimeZone() + old.EndUserTime = &userTime + r.DB.Where("time_span_id = ?", old.ID).Delete(new(model.TimeSpanTag)) + r.DB.Save(old) + + external := timeSpanToExternal(*old) + return external, nil +} diff --git a/timespan/stop_test.go b/timespan/stop_test.go new file mode 100644 index 0000000..8d664a9 --- /dev/null +++ b/timespan/stop_test.go @@ -0,0 +1,111 @@ +package timespan + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func Test_Stop_fail_notExisting(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.StopTimeSpan(fake.User(5), 3, test.ModelTime("2019-06-10T18:35:00+02:00")) + require.Nil(t, timeSpan) + require.EqualError(t, err, "time span with id 3 does not exist") + assertTimeSpanCount(t, db, 0) +} + +func Test_Stop_fail_noPermission(t *testing.T) { + db := test.InMemoryDB(t) + db.User(3) + db.User(2) + db.User(5) + db.Create(&model.TimeSpan{ + ID: 3, + UserID: 3, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + }) + defer db.Close() + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.StopTimeSpan(fake.User(2), 3, test.ModelTime("2019-06-10T18:35:00+02:00")) + require.Nil(t, timeSpan) + require.EqualError(t, err, "time span with id 3 does not exist") + assertTimeSpanCount(t, db, 1) +} + +func Test_Stop_fail_alreadyFinished(t *testing.T) { + db := test.InMemoryDB(t) + db.User(2) + db.User(5) + db.Create(&model.TimeSpan{ + ID: 3, + UserID: 2, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T19:30:00Z"), + EndUTC: test.TimeP("2019-06-10T17:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + }) + defer db.Close() + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.StopTimeSpan(fake.User(2), 3, test.ModelTime("2019-06-10T18:35:00+02:00")) + require.Nil(t, timeSpan) + require.EqualError(t, err, "timespan with id 3 has already an end date") + assertTimeSpanCount(t, db, 1) +} + +func Test_Stop(t *testing.T) { + db := test.InMemoryDB(t) + db.User(2) + db.User(5) + db.Create(&model.TimeSpan{ + ID: 3, + UserID: 2, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + }) + defer db.Close() + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.StopTimeSpan(fake.User(2), 3, test.ModelTime("2019-06-10T18:35:00+02:00")) + require.NoError(t, err) + + expected := &gqlmodel.TimeSpan{ + ID: 3, + Start: test.ModelTime("2019-06-10T18:30:00+02:00"), + End: test.ModelTimeP("2019-06-10T18:35:00+02:00"), + Tags: []*gqlmodel.TimeSpanTag{}, + } + require.Equal(t, expected, timeSpan) + + assertTimeSpanCount(t, db, 1) + assertTimeSpanExist(t, db, model.TimeSpan{ + ID: 3, + UserID: 2, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T18:35:00Z"), + EndUTC: test.TimeP("2019-06-10T16:35:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + }) +} diff --git a/timespan/suggestvalue.go b/timespan/suggestvalue.go new file mode 100644 index 0000000..b98fdc9 --- /dev/null +++ b/timespan/suggestvalue.go @@ -0,0 +1,27 @@ +package timespan + +import ( + "context" + + "github.com/traggo/server/auth" + + "github.com/traggo/server/model" +) + +// SuggestTagValue suggests a tag value. +func (r *ResolverForTimeSpan) SuggestTagValue(ctx context.Context, key string, query string) ([]string, error) { + var suggestions []model.TimeSpanTag + find := r.DB. + Select("DISTINCT time_span_tags.string_value"). + Joins("JOIN time_spans on time_spans.id = time_span_tags.time_span_id"). + Where("user_id = ?", auth.GetUser(ctx).ID). + Where("key = ?", key).Where("LOWER(string_value) LIKE LOWER(?)", "%"+query+"%"). + Limit(10). + Find(&suggestions) + var result []string + for _, value := range suggestions { + result = append(result, value.StringValue) + } + + return result, find.Error +} diff --git a/timespan/suggestvalue_test.go b/timespan/suggestvalue_test.go new file mode 100644 index 0000000..114852d --- /dev/null +++ b/timespan/suggestvalue_test.go @@ -0,0 +1,35 @@ +package timespan + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +const date = "2019-06-10T18:30:00Z" + +func TestGQL_SuggestTagValue(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(1) + user.TimeSpan(date, date).Tag("proj", "gotify").Tag("issue", "3") + user.TimeSpan(date, date).Tag("proj", "traggo").Tag("issue", "3") + user.TimeSpan(date, date).Tag("proj", "traggo").Tag("issue", "3") + user.TimeSpan(date, date).Tag("proj", "meh").Tag("issue", "3") + other := db.User(2) + other.TimeSpan(date, date).Tag("proj", "secret").Tag("issue", "3") + resolver := ResolverForTimeSpan{DB: db.DB} + + tags, err := resolver.SuggestTagValue(fake.User(1), "proj", "") + + require.Nil(t, err) + expected := []string{"gotify", "traggo", "meh"} + require.Equal(t, expected, tags) + + tags, err = resolver.SuggestTagValue(fake.User(1), "proj", "uff") + + require.Nil(t, err) + require.Empty(t, tags) +} diff --git a/timespan/tagcheck.go b/timespan/tagcheck.go new file mode 100644 index 0000000..555b23e --- /dev/null +++ b/timespan/tagcheck.go @@ -0,0 +1,25 @@ +package timespan + +import ( + "fmt" + + "github.com/jinzhu/gorm" + "github.com/traggo/server/model" +) + +func tagsExist(db *gorm.DB, userID int, tags []model.TimeSpanTag) error { + existingTags := make(map[string]struct{}) + + for _, tag := range tags { + if _, ok := existingTags[tag.Key]; ok { + return fmt.Errorf("tag '%s' is present multiple times", tag.Key) + } + + if db.Where("key = ?", tag.Key).Where("user_id = ?", userID).Find(new(model.TagDefinition)).RecordNotFound() { + return fmt.Errorf("tag '%s' does not exist", tag.Key) + } + + existingTags[tag.Key] = struct{}{} + } + return nil +} diff --git a/timespan/timers.go b/timespan/timers.go new file mode 100644 index 0000000..02ba7d3 --- /dev/null +++ b/timespan/timers.go @@ -0,0 +1,27 @@ +package timespan + +import ( + "context" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// Timers returns all running timers for a user +func (r *ResolverForTimeSpan) Timers(ctx context.Context) ([]*gqlmodel.TimeSpan, error) { + user := auth.GetUser(ctx) + + var timeSpans []model.TimeSpan + r.DB.Preload("Tags"). + Where("user_id = ?", user.ID). + Where("end_user_time is null"). + Order("start_user_time DESC"). + Find(&timeSpans) + + result := []*gqlmodel.TimeSpan{} + for _, span := range timeSpans { + result = append(result, timeSpanToExternal(span)) + } + return result, nil +} diff --git a/timespan/timers_test.go b/timespan/timers_test.go new file mode 100644 index 0000000..690e565 --- /dev/null +++ b/timespan/timers_test.go @@ -0,0 +1,25 @@ +package timespan + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestTimers(t *testing.T) { + db := test.InMemoryDB(t) + db.User(5) + db.Create(timeSpan1) + db.Create(runningTimeSpan) + defer db.Close() + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpans, err := resolver.Timers(fake.User(5)) + require.NoError(t, err) + + expected := []*gqlmodel.TimeSpan{&modelRunningTimeSpan} + require.Equal(t, expected, timeSpans) +} diff --git a/timespan/timespanresolver.go b/timespan/timespanresolver.go new file mode 100644 index 0000000..57e49cc --- /dev/null +++ b/timespan/timespanresolver.go @@ -0,0 +1,8 @@ +package timespan + +import "github.com/jinzhu/gorm" + +// ResolverForTimeSpan resolves time span specific things. +type ResolverForTimeSpan struct { + DB *gorm.DB +} diff --git a/timespan/update.go b/timespan/update.go new file mode 100644 index 0000000..cc9540d --- /dev/null +++ b/timespan/update.go @@ -0,0 +1,42 @@ +package timespan + +import ( + "context" + "fmt" + "time" + + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// UpdateTimeSpan update a time span +func (r *ResolverForTimeSpan) UpdateTimeSpan(ctx context.Context, id int, start model.Time, end *model.Time, tags []*gqlmodel.InputTimeSpanTag, oldStart *model.Time, note string) (*gqlmodel.TimeSpan, error) { + timeSpan, err := timespanToInternal(auth.GetUser(ctx).ID, start, end, tags, note) + if err != nil { + return nil, err + } + + oldTimeSpan := model.TimeSpan{ID: id} + if r.DB.Where("user_id = ?", auth.GetUser(ctx).ID).Find(&oldTimeSpan).RecordNotFound() { + return nil, fmt.Errorf("time span with id %d does not exist", id) + } + + if err := tagsExist(r.DB, auth.GetUser(ctx).ID, timeSpan.Tags); err != nil { + return nil, err + } + + timeSpan.ID = id + + r.DB.Where("time_span_id = ?", timeSpan.ID).Delete(new(model.TimeSpanTag)) + r.DB.Save(&timeSpan) + + external := timeSpanToExternal(timeSpan) + if oldStart == nil { + location := time.FixedZone("unknown", oldTimeSpan.OffsetUTC) + old := model.Time(oldTimeSpan.StartUTC.In(location)) + oldStart = &old + } + external.OldStart = oldStart + return external, nil +} diff --git a/timespan/update_test.go b/timespan/update_test.go new file mode 100644 index 0000000..0d50c6f --- /dev/null +++ b/timespan/update_test.go @@ -0,0 +1,251 @@ +package timespan + +import ( + "testing" + + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func Test_Update_withoutEnd(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(3) + note := "A note" + db.Create(&model.TimeSpan{ + ID: 1, + UserID: 3, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + Note: note, + }) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.UpdateTimeSpan(fake.User(3), 1, test.ModelTime("2019-06-10T19:30:00+02:00"), nil, nil, nil, "") + + require.Nil(t, err) + expected := &gqlmodel.TimeSpan{ + ID: 1, + Start: test.ModelTime("2019-06-10T19:30:00+02:00"), + OldStart: test.ModelTimeP("2019-06-10T18:30:00+02:00"), + Tags: []*gqlmodel.TimeSpanTag{}, + Note: "", + } + require.Equal(t, expected, timeSpan) + assertTimeSpanCount(t, db, 1) + assertTimeSpanExist(t, db, model.TimeSpan{ + ID: 1, + UserID: 3, + StartUserTime: test.Time("2019-06-10T19:30:00Z"), + StartUTC: test.Time("2019-06-10T17:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + Note: "", + }) +} + +func Test_Update(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + note := "A note" + db.Create(&model.TimeSpan{ + ID: 3, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + Note: note, + }) + + note = "An upated note" + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.UpdateTimeSpan(fake.User(5), 3, test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T20:30:00+02:00"), nil, nil, note) + log.Debug().Msg("oops") + require.Nil(t, err) + expected := &gqlmodel.TimeSpan{ + ID: 3, + Start: test.ModelTime("2019-06-10T18:30:00+02:00"), + OldStart: test.ModelTimeP("2019-06-10T18:30:00+02:00"), + End: test.ModelTimeP("2019-06-10T20:30:00+02:00"), + Tags: []*gqlmodel.TimeSpanTag{}, + Note: note, + } + require.Equal(t, expected, timeSpan) + assertTimeSpanCount(t, db, 1) + assertTimeSpanExist(t, db, model.TimeSpan{ + ID: 3, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T20:30:00Z"), + EndUTC: test.TimeP("2019-06-10T18:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + Note: note, + }) +} + +func Test_Update_fail_endBeforeStart(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + db.Create(&model.TimeSpan{ + ID: 3, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T18:30:00Z"), + EndUTC: test.TimeP("2019-06-10T16:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + }) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.UpdateTimeSpan(fake.User(5), 3, test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T17:30:00+02:00"), nil, nil, "") + require.Nil(t, timeSpan) + require.EqualError(t, err, "start must be before end") + assertTimeSpanCount(t, db, 1) +} + +func Test_Update_fail_notExistingTag(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + db.Create(&model.TimeSpan{ + ID: 3, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T18:30:00Z"), + EndUTC: test.TimeP("2019-06-10T16:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + }) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.UpdateTimeSpan(fake.User(5), 3, test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T18:35:00+02:00"), []*gqlmodel.InputTimeSpanTag{{Key: "test"}}, nil, "nil") + require.Nil(t, timeSpan) + require.EqualError(t, err, "tag 'test' does not exist") + assertTimeSpanCount(t, db, 1) + assertTimeSpanExist(t, db, model.TimeSpan{ + ID: 3, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T18:30:00Z"), + EndUTC: test.TimeP("2019-06-10T16:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + }) +} + +func Test_Update_withTag(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + db.Create(&model.TimeSpan{ + ID: 3, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T19:30:00Z"), + EndUTC: test.TimeP("2019-06-10T17:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + }) + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.UpdateTimeSpan(fake.User(5), 3, test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T19:30:00+02:00"), []*gqlmodel.InputTimeSpanTag{{Key: "test"}}, nil, "") + require.NotNil(t, timeSpan) + require.NoError(t, err) + assertTimeSpanCount(t, db, 1) + assertTimeSpanExist(t, db, model.TimeSpan{ + ID: 3, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T19:30:00Z"), + EndUTC: test.TimeP("2019-06-10T17:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{ + {Key: "test", TimeSpanID: 3}, + }, + }) +} + +func Test_Update_fail_tagAddedMultipleTimes(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + db.Create(&model.TimeSpan{ + ID: 3, + UserID: 5, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T18:30:00Z"), + EndUTC: test.TimeP("2019-06-10T16:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + }) + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.UpdateTimeSpan(fake.User(5), 3, test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T18:35:00+02:00"), []*gqlmodel.InputTimeSpanTag{{Key: "test"}, {Key: "test"}}, nil, "") + require.Nil(t, timeSpan) + require.EqualError(t, err, "tag 'test' is present multiple times") + assertTimeSpanCount(t, db, 1) +} + +func Test_Update_fail_notExisting(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.User(5) + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.UpdateTimeSpan(fake.User(5), 3, test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T18:35:00+02:00"), []*gqlmodel.InputTimeSpanTag{{Key: "test"}, {Key: "test"}}, nil, "") + require.Nil(t, timeSpan) + require.EqualError(t, err, "time span with id 3 does not exist") + assertTimeSpanCount(t, db, 0) +} + +func Test_Update_fail_noPermission(t *testing.T) { + db := test.InMemoryDB(t) + db.User(3) + db.User(2) + db.User(5) + db.Create(&model.TimeSpan{ + ID: 3, + UserID: 3, + StartUserTime: test.Time("2019-06-10T18:30:00Z"), + StartUTC: test.Time("2019-06-10T16:30:00Z"), + EndUserTime: test.TimeP("2019-06-10T18:30:00Z"), + EndUTC: test.TimeP("2019-06-10T16:30:00Z"), + OffsetUTC: 7200, + Tags: []model.TimeSpanTag{}, + }) + defer db.Close() + db.Create(&model.TagDefinition{Key: "test", UserID: 5}) + + resolver := ResolverForTimeSpan{DB: db.DB} + timeSpan, err := resolver.UpdateTimeSpan(fake.User(2), 3, test.ModelTime("2019-06-10T18:30:00+02:00"), + test.ModelTimeP("2019-06-10T18:35:00+02:00"), []*gqlmodel.InputTimeSpanTag{{Key: "test"}, {Key: "test"}}, nil, "") + require.Nil(t, timeSpan) + require.EqualError(t, err, "time span with id 3 does not exist") + assertTimeSpanCount(t, db, 1) +} diff --git a/timespan/utils_test.go b/timespan/utils_test.go new file mode 100644 index 0000000..b0c442e --- /dev/null +++ b/timespan/utils_test.go @@ -0,0 +1,23 @@ +package timespan + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +func assertTimeSpanExist(t *testing.T, db *test.Database, expected model.TimeSpan) { + found := new(model.TimeSpan) + find := db.Preload("Tags").Where("user_id = ?", expected.UserID).Where("id = ?", expected.ID).Find(found) + require.Nil(t, find.Error) + require.NotNil(t, found) + require.Equal(t, expected, *found) +} + +func assertTimeSpanCount(t *testing.T, db *test.Database, expected int) { + count := new(int) + db.Model(new(model.TimeSpan)).Count(count) + require.Equal(t, expected, *count) +} diff --git a/ui/.prettierignore b/ui/.prettierignore new file mode 100644 index 0000000..6cc99bc --- /dev/null +++ b/ui/.prettierignore @@ -0,0 +1,2 @@ +build +__generated__ \ No newline at end of file diff --git a/ui/.prettierrc b/ui/.prettierrc new file mode 100644 index 0000000..029bca2 --- /dev/null +++ b/ui/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 130, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": false, + "jsxBracketSameLine": true, + "arrowParens": "always" +} \ No newline at end of file diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..0a17cde --- /dev/null +++ b/ui/package.json @@ -0,0 +1,82 @@ +{ + "name": "ui", + "version": "0.1.0", + "private": true, + "homepage": ".", + "dependencies": { + "@apollo/react-hooks": "^3.1.3", + "@date-io/moment": "^1.1.0", + "@fullcalendar/core": "^4.3.1", + "@fullcalendar/daygrid": "^4.3.0", + "@fullcalendar/interaction": "^4.3.0", + "@fullcalendar/moment": "^4.3.0", + "@fullcalendar/react": "^4.3.0", + "@fullcalendar/timegrid": "^4.3.0", + "@material-ui/core": "^4.6.1", + "@material-ui/icons": "^4.5.1", + "@material-ui/pickers": "^3.2.8", + "@rooks/use-interval": "^1.2.0", + "@rooks/use-timeout": "^3.0.1", + "apollo-boost": "^0.4.4", + "color-hash": "^1.0.3", + "downshift": "^3.2.10", + "get-best-contrast-color": "^0.2.2", + "graphql": "^14.5.8", + "lodash.clonedeep": "^4.5.0", + "moment": "^2.30.1", + "moment-timezone": "^0.5.45", + "notistack": "^0.6.1", + "pretty-ms": "^5.0.0", + "react": "^16.12.0", + "react-apollo": "^3.1.3", + "react-color": "3.0.0-beta.3", + "react-dom": "^16.12.0", + "react-grid-layout": "^0.16.6", + "react-infinite": "^0.13.0", + "react-router": "^5.1.2", + "react-router-dom": "^5.1.2", + "recharts": "^1.8.5", + "typeface-roboto": "^0.0.54", + "typescript": "^3.7.2" + }, + "devDependencies": { + "@types/graphql": "^14.0.7", + "@types/jest": "^24.0.15", + "@types/lodash.clonedeep": "^4.5.6", + "@types/moment-timezone": "^0.5.12", + "@types/node": "11.11.3", + "@types/pretty-ms": "^4.0.0", + "@types/react": "16.8.8", + "@types/react-color": "^2.17.0", + "@types/react-dom": "^16.9.4", + "@types/react-grid-layout": "^0.16.7", + "@types/react-infinite": "^0.0.34", + "@types/react-router": "^5.0.1", + "@types/react-router-dom": "^5.1.2", + "@types/recharts": "^1.8.3", + "apollo": "^2.21.0", + "prettier": "^1.19.1", + "react-scripts": "^3.2.0", + "tslint": "^5.20.1", + "tslint-sonarts": "^1.9.0" + }, + "scripts": { + "lint": "tslint --fix --project .", + "lint:check": "tslint --project .", + "generate": "apollo client:codegen --target typescript --localSchemaFile ../schema.graphql --excludes **/*local.ts --globalTypesFile src/gql/__generated__/globalTypes.ts", + "format": "prettier \"**/*.{html,ts,tsx,css}\" --write", + "format:check": "prettier \"**/*.{html,ts,tsx,css}\" --list-different", + "start": "react-scripts start", + "build": "react-scripts build", + "postbuild": "rimraf build/**/*.map", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "proxy": "http://localhost:3030" +} diff --git a/ui/public/apple-touch-icon.png b/ui/public/apple-touch-icon.png new file mode 100644 index 0000000..7fb9d65 Binary files /dev/null and b/ui/public/apple-touch-icon.png differ diff --git a/ui/public/favicon-16x16.png b/ui/public/favicon-16x16.png new file mode 100644 index 0000000..f5d12ba Binary files /dev/null and b/ui/public/favicon-16x16.png differ diff --git a/ui/public/favicon-192x192.png b/ui/public/favicon-192x192.png new file mode 100644 index 0000000..fa9f8db Binary files /dev/null and b/ui/public/favicon-192x192.png differ diff --git a/ui/public/favicon-256x256.png b/ui/public/favicon-256x256.png new file mode 100644 index 0000000..68509c4 Binary files /dev/null and b/ui/public/favicon-256x256.png differ diff --git a/ui/public/favicon-32x32.png b/ui/public/favicon-32x32.png new file mode 100644 index 0000000..a3d72cc Binary files /dev/null and b/ui/public/favicon-32x32.png differ diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 0000000..1a75c4c Binary files /dev/null and b/ui/public/favicon.ico differ diff --git a/ui/public/index.html b/ui/public/index.html new file mode 100644 index 0000000..0432ead --- /dev/null +++ b/ui/public/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + Traggo + + + +
+ + diff --git a/ui/public/manifest.json b/ui/public/manifest.json new file mode 100644 index 0000000..67771bc --- /dev/null +++ b/ui/public/manifest.json @@ -0,0 +1,29 @@ +{ + "name": "Traggo", + "short_name": "Traggo", + "description": "Tag-based time tracking", + "theme_color": "#9bd7fe", + "background_color": "#3f51b5", + "display": "standalone", + "scope": "./", + "start_url": "./", + "icons": [ + { + "src": "/favicon-16x16.png", + "sizes": "16x16", + "type": "image/png" + }, { + "src": "/favicon-32x32.png", + "sizes": "32x32", + "type": "image/png" + }, { + "src": "/favicon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, { + "src": "/favicon-256x256.png", + "sizes": "256x256", + "type": "image/png" + } + ] +} diff --git a/ui/serve.go b/ui/serve.go new file mode 100644 index 0000000..b6462f2 --- /dev/null +++ b/ui/serve.go @@ -0,0 +1,54 @@ +package ui + +import ( + "embed" + "fmt" + "io" + "io/fs" + "net/http" + + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" +) + +//go:embed build +var uiDir embed.FS +var buildDir, _ = fs.Sub(uiDir, "build") + +// Register registers the ui on the root path. +func Register(r *mux.Router) { + r.Handle("/", serveFile("index.html", "text/html")) + r.Handle("/index.html", serveFile("index.html", "text/html")) + r.Handle("/manifest.json", serveFile("manifest.json", "application/json")) + r.Handle("/service-worker.js", serveFile("service-worker.js", "text/javascript")) + r.Handle("/asset-manifest.json", serveFile("asset-manifest.json", "application/json")) + + fileServer := http.FileServer(http.FS(buildDir)) + r.Handle("/static/{type}/{resource}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + fileServer.ServeHTTP(w, r) + })) + + r.Handle("/favicon.ico", serveFile("favicon.ico", "image/x-icon")) + for _, size := range []string{"16x16", "32x32", "192x192", "256x256"} { + fileName := fmt.Sprintf("favicon-%s.png", size) + r.Handle("/"+fileName, serveFile(fileName, "image/png")) + } +} + +func serveFile(name, contentType string) http.HandlerFunc { + file, err := buildDir.Open(name) + if err != nil { + log.Panic().Err(err).Msgf("could not find %s", file) + } + defer file.Close() + content, err := io.ReadAll(file) + if err != nil { + log.Panic().Err(err).Msgf("could not read %s", file) + } + + return func(writer http.ResponseWriter, reg *http.Request) { + writer.Header().Set("Content-Type", contentType) + _, _ = writer.Write(content) + } +} diff --git a/ui/src/Root.tsx b/ui/src/Root.tsx new file mode 100644 index 0000000..de6f984 --- /dev/null +++ b/ui/src/Root.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import './global.css'; +import 'react-resizable/css/styles.css'; +import 'react-grid-layout/css/styles.css'; +import 'typeface-roboto'; +import {ThemeProvider} from './provider/ThemeProvider'; +import {ApolloProvider} from './provider/ApolloProvider'; +import {SnackbarProvider} from './provider/SnackbarProvider'; +import {MuiPickersUtilsProvider} from '@material-ui/pickers'; +import MomentUtils from '@date-io/moment'; +import {Router} from './Router'; +import {HashRouter} from 'react-router-dom'; +import {BootUserSettings} from './provider/UserSettingsProvider'; + +export const Root = () => { + return ( + + + + + + + + + + + + + + ); +}; diff --git a/ui/src/Router.tsx b/ui/src/Router.tsx new file mode 100644 index 0000000..ba97f62 --- /dev/null +++ b/ui/src/Router.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import {useQuery} from '@apollo/react-hooks'; +import {CurrentUser} from './gql/__generated__/CurrentUser'; +import * as gqlUser from './gql/user'; +import {CenteredSpinner} from './common/CenteredSpinner'; +import {LoginPage} from './login/LoginPage'; +import {Typography} from '@material-ui/core'; +import Button from '@material-ui/core/Button'; +import {ApolloError} from 'apollo-boost'; +import Grid from '@material-ui/core/Grid'; +import {DefaultPaper} from './common/DefaultPaper'; +import {Page} from './common/Page'; +import {Redirect, Route, Switch} from 'react-router'; +import {DailyPage} from './timespan/DailyPage'; +import {DevicesPage} from './devices/DevicesPage'; +import {UsersPage} from './user/UsersPage'; +import {DashboardPage} from './dashboard/DashboardPage'; +import {DashboardsPage} from './dashboard/DashboardsPage'; +import {TagPage} from './tag/TagPage'; +import {SettingsPage} from './setting/SettingsPage'; +import {CalendarPage} from './timespan/calendar/CalendarPage'; + +export const Router = () => { + const {loading, error, data, refetch} = useQuery(gqlUser.CurrentUser); + if (loading) { + return ; + } + if (error) { + return ; + } + const loggedIn = data && data.user; + const admin = data && data.user && data.user.admin; + + return ( + + {loggedIn ? : } + {loggedIn ? null : } + + + + + + + + + + {admin ? : null} + } /> + + + ); +}; + +const Error: React.FC<{error: ApolloError; refetch: () => void}> = ({error, refetch}) => { + return ( + + + + + Error + + + {error.networkError && error.networkError.name + ': ' + error.networkError.message} + {error.graphQLErrors.map((gqlError) => gqlError.message).join(', ')} + + + + + + ); +}; + +export const TODO = () => { + return ( + + TODO + + ); +}; diff --git a/ui/src/common/Center.tsx b/ui/src/common/Center.tsx new file mode 100644 index 0000000..a45585e --- /dev/null +++ b/ui/src/common/Center.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; + +export const Center: React.FC = ({children}) => { + return
{children}
; +}; diff --git a/ui/src/common/CenteredSpinner.tsx b/ui/src/common/CenteredSpinner.tsx new file mode 100644 index 0000000..6a87537 --- /dev/null +++ b/ui/src/common/CenteredSpinner.tsx @@ -0,0 +1,11 @@ +import CircularProgress from '@material-ui/core/CircularProgress'; +import Grid from '@material-ui/core/Grid'; +import * as React from 'react'; + +export const CenteredSpinner = () => ( + + + + + +); diff --git a/ui/src/common/ConfirmDialog.tsx b/ui/src/common/ConfirmDialog.tsx new file mode 100644 index 0000000..f0cc066 --- /dev/null +++ b/ui/src/common/ConfirmDialog.tsx @@ -0,0 +1,36 @@ +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import React from 'react'; + +interface Props { + title: string; + fClose: () => void; + fOnSubmit: () => void; +} + +export const ConfirmDialog: React.FC = ({children, title, fClose, fOnSubmit}) => { + const submitAndClose = () => { + fOnSubmit(); + fClose(); + }; + return ( + + {title} + + {children} + + + + + + + ); +}; diff --git a/ui/src/common/DateTimeSelector.tsx b/ui/src/common/DateTimeSelector.tsx new file mode 100644 index 0000000..689039e --- /dev/null +++ b/ui/src/common/DateTimeSelector.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import {KeyboardDateTimePicker} from '@material-ui/pickers'; +import * as moment from 'moment'; +import {uglyConvertToLocalTime} from '../timespan/timeutils'; +import {useSettings} from '../gql/settings'; +import {DateTimeInputStyle} from '../gql/__generated__/globalTypes'; + +interface DateTimeSelectorProps { + selectedDate: moment.Moment; + onSelectDate: (date: moment.Moment) => void; + showDate: boolean; + label: string; + popoverOpen?: (open: boolean) => void; +} + +export const DateTimeSelector: React.FC = React.memo( + ({selectedDate, onSelectDate, showDate, label, popoverOpen = () => {}}) => { + const {done, dateTimeInputStyle} = useSettings(); + + if (!done) { + return ...; + } + + if (dateTimeInputStyle === DateTimeInputStyle.Native) { + return ( + { + onSelectDate(moment.default(e.target.value)); + }} + /> + ); + } + + const [open, setOpen] = React.useState(false); + const localeData = moment.localeData(); + const time = localeData.longDateFormat('LT').replace('A', 'a'); + const ampm = time.indexOf('a') !== -1; + const format = showDate ? localeData.longDateFormat('L') + ' ' + time : time; + + return ( + { + popoverOpen(true); + setOpen(true); + }, + onExited: () => { + popoverOpen(false); + setOpen(false); + }, + }} + margin="none" + value={uglyConvertToLocalTime(selectedDate).format()} + onChange={(date: moment.Moment) => { + if (!date || !date.isValid()) { + return; + } + + if (!showDate && !open) { + date = date.set({ + date: selectedDate.date(), + month: selectedDate.month(), + year: selectedDate.year(), + }); + } + if (uglyConvertToLocalTime(selectedDate).isSame(date)) { + return; + } + + onSelectDate(date); + }} + ampm={ampm} + format={format} + label={label} + openTo={showDate ? 'date' : 'hours'} + /> + ); + } +); diff --git a/ui/src/common/DefaultPaper.tsx b/ui/src/common/DefaultPaper.tsx new file mode 100644 index 0000000..bd27a11 --- /dev/null +++ b/ui/src/common/DefaultPaper.tsx @@ -0,0 +1,22 @@ +import Paper from '@material-ui/core/Paper'; +import {makeStyles} from '@material-ui/core/styles'; +import * as React from 'react'; + +const useStyles = makeStyles((theme) => ({ + root: { + ...theme.mixins.gutters(), + paddingTop: theme.spacing(4), + paddingBottom: theme.spacing(3), + textAlign: 'center', + maxWidth: 400, + borderTop: `5px solid ${theme.palette.primary.main}`, + }, +})); +export const DefaultPaper: React.FC = ({children}) => { + const classes = useStyles(); + return ( + + {children} + + ); +}; diff --git a/ui/src/common/Fade.tsx b/ui/src/common/Fade.tsx new file mode 100644 index 0000000..33c3168 --- /dev/null +++ b/ui/src/common/Fade.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import {CSSTransition} from 'react-transition-group'; + +const duration = 100; + +const defaultStyle = { + transition: `all ${duration}ms ease-in-out`, + opacity: 0, +}; + +export const Fade: React.FC<{fullyVisible: boolean; opacity?: number}> = ({fullyVisible, children, opacity = 0.4}) => { + const transitionStyles: Record = { + entering: {opacity: 1}, + entered: {opacity: 1}, + exiting: {opacity}, + exited: {opacity}, + }; + return ( + + {(state) => ( +
+ {children} +
+ )} +
+ ); +}; diff --git a/ui/src/common/Page.tsx b/ui/src/common/Page.tsx new file mode 100644 index 0000000..69111e3 --- /dev/null +++ b/ui/src/common/Page.tsx @@ -0,0 +1,308 @@ +import * as React from 'react'; +import AppBar from '@material-ui/core/AppBar'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import Divider from '@material-ui/core/Divider'; +import Drawer from '@material-ui/core/Drawer'; +import Hidden from '@material-ui/core/Hidden'; +import IconButton from '@material-ui/core/IconButton'; +import UsersIcon from '@material-ui/icons/SupervisorAccount'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import DashboardIcon from '@material-ui/icons/Dashboard'; +import MenuIcon from '@material-ui/icons/Menu'; +import SettingsIcon from '@material-ui/icons/Settings'; +import DevicesIcon from '@material-ui/icons/DevicesOther'; +import LabelIcon from '@material-ui/icons/Label'; +import DashboardManageIcon from '@material-ui/icons/ListAlt'; +import TimeLineIcon from '@material-ui/icons/Timeline'; +import CalendarIcon from '@material-ui/icons/CalendarToday'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import {ListSubheader, Menu} from '@material-ui/core'; +import HrefLink from '@material-ui/core/Link'; +import * as gqlUser from '../gql/user'; +import * as gqlVersion from '../gql/version'; +import Button from '@material-ui/core/Button'; +import AccountCircle from '@material-ui/icons/AccountCircle'; +import {Link} from 'react-router-dom'; +import MenuItem from '@material-ui/core/MenuItem'; +import {useMutation, useQuery} from '@apollo/react-hooks'; +import {Logout} from '../gql/__generated__/Logout'; +import {Version} from '../gql/__generated__/Version'; +import {CurrentUser} from '../gql/__generated__/CurrentUser'; +import * as gqlDashboard from '../gql/dashboard'; +import {Dashboards} from '../gql/__generated__/Dashboards'; +import makeStyles from '@material-ui/core/styles/makeStyles'; +import {Route, RouteChildrenProps, Switch} from 'react-router'; + +const drawerWidth = 240; + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + height: '100%', + }, + drawer: { + [theme.breakpoints.up('md')]: { + width: drawerWidth, + flexShrink: 0, + }, + }, + appBar: { + marginLeft: drawerWidth, + [theme.breakpoints.up('md')]: { + width: `calc(100% - ${drawerWidth}px)`, + }, + }, + menuButton: { + marginRight: 20, + [theme.breakpoints.up('md')]: { + display: 'none', + }, + }, + toolbar: { + ...theme.mixins.toolbar, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + }, + drawerPaper: { + width: drawerWidth, + }, + content: { + flexGrow: 1, + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + }, + grow: { + flexGrow: 1, + }, + sectionDesktop: { + display: 'none', + [theme.breakpoints.up('xs')]: { + display: 'flex', + }, + }, + sectionMobile: { + display: 'flex', + [theme.breakpoints.up('xs')]: { + display: 'none', + }, + }, +})); + +// tslint:disable-next-line:no-any +const routerLink = (to: string): any => { + return React.forwardRef((props, ref) => ); +}; + +export const Page: React.FC = ({children}) => { + const classes = useStyles(); + const {data} = useQuery(gqlUser.CurrentUser); + + const [mobileOpen, setMobileOpen] = React.useState(false); + const [userMenuOpen, setUserMenuOpen] = React.useState(null); + const [logout] = useMutation(gqlUser.Logout, {refetchQueries: [{query: gqlUser.CurrentUser}]}); + const {data: {version = gqlVersion.VersionDefault.version} = gqlVersion.VersionDefault} = useQuery( + gqlVersion.Version + ); + const dashboardsQuery = useQuery(gqlDashboard.Dashboards); + const dashboards = (dashboardsQuery.data && dashboardsQuery.data.dashboards) || []; + + const username = (data && data.user && data.user.name) || 'unknown'; + const admin = data && data.user && data.user.admin; + + const drawer = ( +
+
+ + + traggo + + + + + {version.name}@{version.commit.slice(0, 8)} + + +
+ + Dashboards} dense={true}> + {dashboards.map(({id, name}) => ( + + + + + + + ))} + {dashboards.length === 0 ? ( + + + + ) : null} + + + + + + + + + Timesheet} dense={true}> + + + + + + + + + + + + + + + User} dense={true}> + + + + + + + + + + + + + + + + + + + + {admin ? ( + <> + + Admin} dense={true}> + + + + + + + + + ) : null} + +
+ ); + + return ( +
+ + + + setMobileOpen(!mobileOpen)} + className={classes.menuButton}> + + + + + + Timesheet / List + + + Timesheet / Calendar + + + User / Settings + + + User / Devices + + + User / Tags + + + Admin / Users + + + Dashboards / Manage + + + {(props: RouteChildrenProps<{id: string}>) => { + const db = dashboards.find( + (dashboard) => dashboard.id === parseInt(props.match!.params.id, 10) + ); + return 'Dashboards / ' + (db ? db.name : '...'); + }} + + + +
+
+ + setUserMenuOpen(null)}> + setUserMenuOpen(null)} component={routerLink('/user/settings')}> + Settings + + { + setUserMenuOpen(null); + logout(); + }}> + Logout + + +
+ + + +
+
+ {children} +
+
+ ); +}; diff --git a/ui/src/common/RelativeDateTimeSelector.tsx b/ui/src/common/RelativeDateTimeSelector.tsx new file mode 100644 index 0000000..0ca541c --- /dev/null +++ b/ui/src/common/RelativeDateTimeSelector.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import {TextField} from '@material-ui/core'; +import {parseRelativeTime} from '../utils/time'; +import Typography from '@material-ui/core/Typography'; +import useTimeout from '@rooks/use-timeout'; + +interface RelativeDateTimeSelectorProps { + value: string; + onChange: (value: string, valid: boolean) => void; + type: 'startOf' | 'endOf'; + label?: string; + disabled?: boolean; + disableUnderline?: boolean; + style?: React.CSSProperties; + small?: boolean; +} + +export const RelativeDateTimeSelector: React.FC = ({ + value, + onChange: setValue, + type, + style, + label, + small = false, + disableUnderline = false, + disabled = false, +}) => { + const [errVisible, setErrVisible] = React.useState(false); + const [error, setError] = React.useState(''); + const {start, stop} = useTimeout(() => setErrVisible(true), 200); + + const parsed = parseRelativeTime(value, type); + return ( + { + const newValue = e.target.value; + const result = parseRelativeTime(newValue, type); + setErrVisible(false); + stop(); + if (!result.success) { + setError(result.error); + start(); + } else { + setError(''); + } + setValue(newValue, result.success); + }} + error={error !== ''} + helperText={ + small ? ( + undefined + ) : errVisible ? ( + + {error} + + ) : ( + {!parsed.success ? '...' : parsed.value.format('llll')} + ) + } + label={label} + /> + ); +}; diff --git a/ui/src/common/RelativeTime.tsx b/ui/src/common/RelativeTime.tsx new file mode 100644 index 0000000..c59dcdc --- /dev/null +++ b/ui/src/common/RelativeTime.tsx @@ -0,0 +1,21 @@ +import moment from 'moment'; +import * as React from 'react'; +import {timeRunning} from '../timespan/timeutils'; +import useInterval from '@rooks/use-interval'; + +export const RelativeToNow: React.FC<{from: moment.Moment}> = ({from}) => { + const [now, setNow] = React.useState(moment()); + + useInterval( + () => { + setNow(moment()); + }, + 1000, + true + ); + return ; +}; + +export const RelativeTime: React.FC<{from: moment.Moment; to: moment.Moment}> = ({from, to}) => { + return <>{timeRunning(from, to)}; +}; diff --git a/ui/src/common/TagChip.tsx b/ui/src/common/TagChip.tsx new file mode 100644 index 0000000..66f1239 --- /dev/null +++ b/ui/src/common/TagChip.tsx @@ -0,0 +1,37 @@ +import Chip from '@material-ui/core/Chip'; +import * as React from 'react'; +// @ts-ignore +import bestContrast from 'get-best-contrast-color'; +import {makeStyles, Theme} from '@material-ui/core'; + +const useStyles = makeStyles((theme: Theme) => ({ + chip: { + margin: theme.spacing(0.5, 0.6), + cursor: 'text', + minHeight: '32px', + height: 'fit-content', + whiteSpace: 'normal', + wordBreak: 'break-word', + }, +})); + +interface TagChipProps { + label: string; + color: string; + onClick?: (e: React.MouseEvent) => void; +} + +export const TagChip: React.FC = ({color, label, onClick}) => { + const classes = useStyles(); + const textColor = bestContrast(color, ['#fff', '#000']); + return ( + + ); +}; diff --git a/ui/src/common/tsutil.ts b/ui/src/common/tsutil.ts new file mode 100644 index 0000000..1661337 --- /dev/null +++ b/ui/src/common/tsutil.ts @@ -0,0 +1,2 @@ +export type Diff = ({[P in T]: P} & {[P in U]: never} & {[x: string]: never})[T]; +export type Omit> = Pick, K>>; diff --git a/ui/src/dashboard/AddDashboardDialog.tsx b/ui/src/dashboard/AddDashboardDialog.tsx new file mode 100644 index 0000000..33926a0 --- /dev/null +++ b/ui/src/dashboard/AddDashboardDialog.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import {useMutation} from '@apollo/react-hooks'; +import {useSnackbar} from 'notistack'; +import {handleError} from '../utils/errors'; +import * as gqlDashboard from '../gql/dashboard'; +import {CreateDashboard, CreateDashboardVariables} from '../gql/__generated__/CreateDashboard'; + +interface AddTagDialogProps { + open: boolean; + close: () => void; +} + +export const AddDashboardDialog: React.FC = ({close, open}) => { + const [name, setName] = React.useState(''); + const {enqueueSnackbar} = useSnackbar(); + + const [addUser] = useMutation(gqlDashboard.CreateDashboard, { + refetchQueries: [{query: gqlDashboard.Dashboards}], + }); + const submit = (e: React.FormEvent) => { + e.preventDefault(); + addUser({variables: {name}}) + .then(() => { + enqueueSnackbar('Dashboard created', {variant: 'success'}); + close(); + }) + .catch(handleError('Create Dashboard', enqueueSnackbar)); + }; + + return ( + +
+ Create Dashboard + + setName(e.target.value)} + /> + + + + + +
+
+ ); +}; diff --git a/ui/src/dashboard/DashboardPage.tsx b/ui/src/dashboard/DashboardPage.tsx new file mode 100644 index 0000000..cc623dd --- /dev/null +++ b/ui/src/dashboard/DashboardPage.tsx @@ -0,0 +1,293 @@ +import * as React from 'react'; +import {useMutation, useQuery} from '@apollo/react-hooks'; +import * as gqlDashboard from '../gql/dashboard'; +import {default as ReactGrid, Layout, WidthProvider} from 'react-grid-layout'; +import {Dashboards, Dashboards_dashboards_items} from '../gql/__generated__/Dashboards'; +import {UpdatePos, UpdatePosVariables} from '../gql/__generated__/UpdatePos'; +import {EntryType, StatsInterval} from '../gql/__generated__/globalTypes'; +import {DashboardEntry} from './Entry/DashboardEntry'; +import Button from '@material-ui/core/Button'; +import clone from 'lodash.clonedeep'; +import {EditPopup} from './Entry/EditPopup'; +import {EditGlass} from './Entry/EditGlass'; +import {Fade} from '../common/Fade'; +import {CenteredSpinner} from '../common/CenteredSpinner'; +import {AddPopup} from './Entry/AddPopup'; +import {Paper} from '@material-ui/core'; +import {Center} from '../common/Center'; +import {RemoveDashboardEntry, RemoveDashboardEntryVariables} from '../gql/__generated__/RemoveDashboardEntry'; +import {RouteChildrenProps} from 'react-router'; +import {useSnackbar} from 'notistack'; +import {DateRanges} from './DateRanges'; +import {Range} from '../utils/range'; +import {useStateAndDelegateWithDelayOnChange} from '../utils/hooks'; + +enum ViewType { + Mobile = 'mobile', + Desktop = 'desktop', +} + +const cols: Record = { + [ViewType.Mobile]: 4, + [ViewType.Desktop]: 20, +}; + +const WidthAwareReactGrid = WidthProvider(ReactGrid); +const EditId = -1; +const newEntry = (): Dashboards_dashboards_items => { + return { + __typename: 'DashboardEntry', + title: '', + id: EditId, + entryType: EntryType.PieChart, + statsSelection: { + __typename: 'StatsSelection', + interval: StatsInterval.Single, + range: { + __typename: 'RelativeOrStaticRange', + from: 'now-1w/w', + to: 'now-1w/w', + }, + rangeId: null, + tags: [], + excludeTags: [], + includeTags: [], + }, + pos: { + __typename: 'ResponsiveDashboardEntryPos', + [ViewType.Desktop]: { + x: 0, + y: 0, + h: 6, + w: 6, + minH: 0, + minW: 0, + __typename: 'DashboardEntryPos', + }, + [ViewType.Mobile]: { + x: 0, + y: 0, + h: 5, + w: 2, + minH: 0, + minW: 0, + __typename: 'DashboardEntryPos', + }, + }, + total: false, + }; +}; + +type RouterProps = RouteChildrenProps<{id?: string}>; + +export const DashboardPage: React.FC = ({match, history}) => { + const [addRef, setAddRef] = React.useState(null); + const endRef = React.useRef(null); + const [changeMode, setChangeMode] = React.useState(false); + const [viewType, setViewType] = React.useState(ViewType.Desktop); + const [preview, setPreview] = React.useState(false); + const [diagramRanges, setDiagramRanges] = React.useState>({}); + const [ranges, setRanges] = useStateAndDelegateWithDelayOnChange>({}, setDiagramRanges, 2000); + const [addEntry, setAddEntry] = React.useState(null); + const [[editElement, editEntry], setEdit] = React.useState<[null] | [HTMLElement, Dashboards_dashboards_items]>([null]); + const {loading, data, error} = useQuery(gqlDashboard.Dashboards); + const [updatePos] = useMutation(gqlDashboard.UpdatePos, { + refetchQueries: [{query: gqlDashboard.Dashboards}], + }); + const [removeDashboardEntry] = useMutation( + gqlDashboard.RemoveDashboardEntry, + { + refetchQueries: [{query: gqlDashboard.Dashboards}], + } + ); + const {enqueueSnackbar} = useSnackbar(); + if (loading) { + return ; + } + if (error || !data) { + return error; + } + + const dashboards = data.dashboards || []; + + if (!match || !match.params.id) { + enqueueSnackbar('id parameter is missing in url', {variant: 'warning'}); + history.push('/dashboards'); + return <>; + } + const dashboard = dashboards.find((db) => '' + db.id === match.params.id); + if (!dashboard) { + enqueueSnackbar('dashboard does not exist', {variant: 'warning'}); + history.push('/dashboards'); + return <>; + } + + const dashboardEntries = dashboard.items; + + const l: Layout[] = dashboard.items.map((item) => ({ + i: '' + item.id, + x: item.pos[viewType].x, + w: item.pos[viewType].w, + h: item.pos[viewType].h, + y: item.pos[viewType].y, + minH: item.pos[viewType].minH, + minW: item.pos[viewType].minW, + })); + const maxY = l.reduce((a, b) => Math.max(a, b.y + b.h), 0); + if (addEntry) { + const newEntryLayout: Layout = {i: '' + EditId, ...addEntry.pos[viewType], y: maxY, static: true}; + l.push(newEntryLayout); + } + + const rangeToName = () => + dashboard.ranges.reduce( + (a, range) => ({...a, [range.id]: `${range.name} (${range.range.from} - ${range.range.to})`}), + {} + ); + + const indexedRanges = dashboard.ranges.reduce((a, range) => ({...a, [range.id]: diagramRanges[range.id] || range.range}), {}); + + const updatePosHandler = (_1: unknown, _2: unknown, newItem: Layout) => { + updatePos({ + variables: { + entryId: parseInt(newItem.i!, 10), + pos: {[viewType]: {x: newItem.x, y: newItem.y, w: newItem.w, h: newItem.h}}, + }, + }).then(console.log); + }; + + return ( + <> +
+
+ +
+
+ {changeMode ? ( + + ) : null} + {changeMode ? ( + + ) : null} + +
+
+
+ + {dashboardEntries.map((entry) => { + const currentEditedAndPreviewed = preview && editEntry && editEntry.id === entry.id; + return ( +
+ + {changeMode ? ( + + setEdit([elm, clone(entry)])} + doDelete={() => removeDashboardEntry({variables: {id: entry.id}})} + /> + + ) : null} +
+ ); + })} + {addEntry ? ( +
{ + setAddRef(ref); + if (ref) { + ref.scrollIntoView({behavior: 'smooth'}); + } + }}> + {preview ? ( + + ) : ( + +
New Entry
+
+ )} + + {addRef ? ( + { + return setAddEntry(clone(e)); + }} + finish={() => { + setAddEntry(null); + setAddRef(null); + }} + /> + ) : null} +
+ ) : ( + [] + )} +
+
+ {editElement !== null && editEntry !== null ? ( + { + if (entry === null) { + setEdit([null]); + } else { + setEdit([editElement, entry]); + } + }} + /> + ) : null} +
+ + ); +}; diff --git a/ui/src/dashboard/DashboardsPage.tsx b/ui/src/dashboard/DashboardsPage.tsx new file mode 100644 index 0000000..0911bd1 --- /dev/null +++ b/ui/src/dashboard/DashboardsPage.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +import Paper from '@material-ui/core/Paper'; +import {useMutation, useQuery} from '@apollo/react-hooks'; +import {CenteredSpinner} from '../common/CenteredSpinner'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import DeleteIcon from '@material-ui/icons/Delete'; +import EditIcon from '@material-ui/icons/Edit'; +import DoneIcon from '@material-ui/icons/Done'; +import CloseIcon from '@material-ui/icons/Close'; +import IconButton from '@material-ui/core/IconButton'; +import {useSnackbar} from 'notistack'; +import * as gqlDashboard from '../gql/dashboard'; +import {TextField} from '@material-ui/core'; +import Button from '@material-ui/core/Button'; +import {Dashboards} from '../gql/__generated__/Dashboards'; +import {RemoveDashboard, RemoveDashboardVariables} from '../gql/__generated__/RemoveDashboard'; +import {UpdateDashboard, UpdateDashboardVariables} from '../gql/__generated__/UpdateDashboard'; +import {AddDashboardDialog} from './AddDashboardDialog'; +import makeStyles from '@material-ui/core/styles/makeStyles'; +import {ConfirmDialog} from '../common/ConfirmDialog'; + +const useStyles = makeStyles((theme) => ({ + root: { + ...theme.mixins.gutters(), + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(3), + textAlign: 'center', + maxWidth: 800, + margin: '0 auto', + }, +})); + +const NoEdit = [-1, ''] as const; + +export const DashboardsPage = () => { + const classes = useStyles(); + const {loading, data} = useQuery(gqlDashboard.Dashboards); + const [addDashboard, setAddDashboard] = React.useState(false); + const [removeDashboardConfirm, setRemoveDashboardConfirm] = React.useState(false); + const refetch = {refetchQueries: [{query: gqlDashboard.Dashboards}]}; + const {enqueueSnackbar} = useSnackbar(); + const [removeDashboard] = useMutation(gqlDashboard.RemoveDashboard, refetch); + const [[editId, editName], setEditing] = React.useState>(NoEdit); + const [updateDashboard] = useMutation(gqlDashboard.UpdateDashboard, refetch); + if (loading || !data || !data.dashboards) { + return ; + } + const onClickDelete = () => { + if (removeDashboardConfirm) { + removeDashboard({variables: {id: removeDashboardConfirm[0]}}).then(() => + enqueueSnackbar('dashboard deleted', {variant: 'success'}) + ); + } + }; + + const dashboards = data.dashboards.map((dashboard) => { + const onClickSubmit = () => { + setEditing(NoEdit); + updateDashboard({ + variables: { + id: editId, + name: editName, + }, + }).then(() => enqueueSnackbar('dashboard edited', {variant: 'success'})); + }; + const isEdited = editId === dashboard.id; + return ( + + {dashboard.id} + + {isEdited ? ( + setEditing([editId, e.target.value])} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onClickSubmit(); + } + }} + onSubmit={onClickSubmit} + /> + ) : ( + dashboard.name + )} + + + {isEdited ? ( + <> + + + + setEditing(NoEdit)} title="Cancel"> + + + + ) : ( + <> + setEditing([dashboard.id, dashboard.name])} title="Edit"> + + + setRemoveDashboardConfirm([dashboard.id, dashboard.name])} title="Delete"> + + + + )} + + + ); + }); + + return ( + + + {addDashboard && setAddDashboard(false)} />} + + + + ID + Name + + + + {dashboards} +
+ {removeDashboardConfirm ? ( + setRemoveDashboardConfirm(false)} + fOnSubmit={onClickDelete}> + This operation cannot be undone. Deleting the dashboard will remove all its dashboard entries. + + ) : null} +
+ ); +}; diff --git a/ui/src/dashboard/DateRanges.tsx b/ui/src/dashboard/DateRanges.tsx new file mode 100644 index 0000000..ec28200 --- /dev/null +++ b/ui/src/dashboard/DateRanges.tsx @@ -0,0 +1,213 @@ +import * as React from 'react'; +import {useMutation} from '@apollo/react-hooks'; +import * as gqlDashboard from '../gql/dashboard'; +import {Dashboards_dashboards, Dashboards_dashboards_ranges} from '../gql/__generated__/Dashboards'; +import {IconButton, Paper, Typography} from '@material-ui/core'; +import {RelativeDateTimeSelector} from '../common/RelativeDateTimeSelector'; +import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; +import MoreVert from '@material-ui/icons/MoreVert'; +import PlusIcon from '@material-ui/icons/Add'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import {RemoveDashboardRange, RemoveDashboardRangeVariables} from '../gql/__generated__/RemoveDashboardRange'; +import {UpdateDashboardRange, UpdateDashboardRangeVariables} from '../gql/__generated__/UpdateDashboardRange'; +import {stripTypename} from '../utils/strip'; +import {Range} from '../utils/range'; +import {AddDashboardRange, AddDashboardRangeVariables} from '../gql/__generated__/AddDashboardRange'; +import Input from '@material-ui/core/Input'; + +interface Props { + changeMode: boolean; + dashboard: Dashboards_dashboards; + setRanges: (cb: (ranges: Record) => Record) => void; + ranges: Record; +} + +export const DateRanges: React.FC = ({changeMode, dashboard, ranges, setRanges}) => { + const saveRef = React.useRef(null); + const [editedRanges, setEditedRanges] = React.useState>({}); + const [editedNames, setEditedNames] = React.useState>({}); + const [openMenu, setOpenMenu] = React.useState(null); + + const [removeRange] = useMutation(gqlDashboard.RemoveDashboardRange, { + refetchQueries: [{query: gqlDashboard.Dashboards}], + }); + const [updateRange] = useMutation(gqlDashboard.UpdateDashboardRange, { + refetchQueries: [{query: gqlDashboard.Dashboards}], + }); + const [addRange] = useMutation(gqlDashboard.AddDashboardRange, { + refetchQueries: [{query: gqlDashboard.Dashboards}], + }); + const saveRanges = (range: Dashboards_dashboards_ranges, newRange: Range, newName: string) => { + if (saveRef.current) { + clearTimeout(saveRef.current); + } + saveRef.current = window.setTimeout(() => { + updateRange({ + variables: { + rangeId: range.id, + range: { + editable: !range.editable, + range: newRange, + name: newName, + }, + }, + }); + }, 500); + }; + React.useEffect(() => { + if (changeMode) { + setEditedRanges({}); + setEditedNames({}); + } + }, [changeMode]); + + return ( + <> + {dashboard.ranges + .filter((range) => range.editable || changeMode) + .map((range) => { + const name = editedNames[range.id] !== undefined ? editedNames[range.id] : range.name; + const dateRange = (changeMode ? editedRanges[range.id] : ranges[range.id]) || stripTypename(range.range); + return ( + + +
+ + {changeMode ? ( + { + const value = e.target.value; + setEditedNames((old) => ({ + ...old, + [range.id]: value, + })); + saveRanges(range, dateRange, value); + }} + value={name} + /> + ) : ( + name + )} + + { + (changeMode ? setEditedRanges : setRanges)((old) => ({ + ...old, + [range.id]: {...dateRange, from: value}, + })); + if (valid && changeMode) { + saveRanges(range, {...dateRange, from: value}, name); + } + }} + type="startOf" + /> + + { + (changeMode ? setEditedRanges : setRanges)((old) => ({ + ...old, + [range.id]: {...dateRange, to: value}, + })); + if (valid && changeMode) { + saveRanges( + range, + { + ...dateRange, + to: value, + }, + name + ); + } + }} + type="endOf" + /> +
+ {changeMode ? ( + <> + + setOpenMenu([e.currentTarget, range.id])}> + + + + + ) : ( + undefined + )} + {openMenu && openMenu[1] === range.id ? ( + setOpenMenu(null)} + open={openMenu !== null}> + { + const used = dashboard.items.filter( + (entry) => entry.statsSelection.rangeId === range.id + ); + if (used.length === 0) { + removeRange({variables: {rangeId: range.id}}); + } else { + alert('Range is used by ' + used.map((entry) => entry.title).join(', ')); + } + setOpenMenu(null); + }}> + Delete + + { + updateRange({ + variables: { + rangeId: range.id, + range: { + editable: !range.editable, + range: stripTypename(range.range), + name: range.name, + }, + }, + }); + setOpenMenu(null); + }}> + {range.editable ? 'make static' : 'make editable'} + + + ) : ( + undefined + )} +
+
+ ); + })} + {changeMode ? ( + + { + addRange({ + variables: { + dashboardId: dashboard.id, + range: {name: 'new range', editable: true, range: {from: 'now/w', to: 'now/w'}}, + }, + }); + }}> + + + + ) : ( + undefined + )} + + ); +}; diff --git a/ui/src/dashboard/Entry/AddPopup.tsx b/ui/src/dashboard/Entry/AddPopup.tsx new file mode 100644 index 0000000..43bf878 --- /dev/null +++ b/ui/src/dashboard/Entry/AddPopup.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import Popper from '@material-ui/core/Popper'; +import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import {Paper} from '@material-ui/core'; +import Typography from '@material-ui/core/Typography'; +import Button from '@material-ui/core/Button'; +import {Dashboards_dashboards_items} from '../../gql/__generated__/Dashboards'; +import {useMutation} from '@apollo/react-hooks'; +import * as gqlDashboard from '../../gql/dashboard'; +import {Fade} from '../../common/Fade'; +import {DashboardEntryForm, isValidDashboardEntry} from './DashboardEntryForm'; +import {AddDashboardEntry, AddDashboardEntryVariables} from '../../gql/__generated__/AddDashboardEntry'; +import {handleError} from '../../utils/errors'; +import {useSnackbar} from 'notistack'; + +interface EditPopupProps { + dashboardId: number; + entry: Dashboards_dashboards_items; + anchorEl: HTMLElement; + onChange: (entry: Dashboards_dashboards_items | null) => void; + doPreview: (preview: boolean) => void; + preview: boolean; + maxY: number; + finish: () => void; + ranges: Record; +} + +export const AddPopup: React.FC = ({ + entry, + maxY, + anchorEl, + onChange: setEdit, + doPreview, + preview, + dashboardId, + finish, + ranges, +}) => { + const [addEntry] = useMutation(gqlDashboard.AddDashboardEntry, { + refetchQueries: [{query: gqlDashboard.Dashboards}], + }); + const valid = isValidDashboardEntry(entry); + + const {enqueueSnackbar} = useSnackbar(); + + return ( + + + setEdit(null)}> + + Add + +
+ + + +
+
+
+
+
+ ); +}; diff --git a/ui/src/dashboard/Entry/DashboardBarChart.tsx b/ui/src/dashboard/Entry/DashboardBarChart.tsx new file mode 100644 index 0000000..58a45c3 --- /dev/null +++ b/ui/src/dashboard/Entry/DashboardBarChart.tsx @@ -0,0 +1,69 @@ +import {Stats_stats} from '../../gql/__generated__/Stats'; +import {Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import * as React from 'react'; +import {Colors} from './colors'; +import {ofSeconds} from './unit'; +import {ofInterval} from './dateformat'; +import {StatsInterval} from '../../gql/__generated__/globalTypes'; +import moment from 'moment'; +import {TagTooltip} from './TagTooltip'; + +interface DashboardPieChartProps { + entries: Stats_stats[]; + interval: StatsInterval; + type: 'stacked' | 'normal'; + total: boolean; +} + +interface Indexed { + start: string; + end: string; + data: Record; +} + +export const DashboardBarChart: React.FC = ({entries, interval, type, total}) => { + const indexedEntries: Indexed[] = entries + .map((entry) => { + return { + start: entry.start, + end: entry.end, + data: entry.entries!.reduce((all: Record, current) => { + return {...all, [current.key + ':' + current.value]: current.timeSpendInSeconds}; + }, {}), + }; + }) + .sort((left, right) => moment(left.start).diff(right.start)); + const dataMax = indexedEntries.reduce((max, entry) => { + return Math.max( + max, + Object.values(entry.data).reduce((a: number, b: number) => Math.max(a, b), 0), + 0 + ); + }, 0); + const unit = ofSeconds(dataMax); + const dateFormat = ofInterval(interval); + return ( + + + + + } /> + + dateFormat(moment(entry.start))} interval={'preserveStartEnd'} /> + + {indexedEntries[0] && + Object.keys(indexedEntries[0].data).map((key, index) => { + return ( + unit.toUnit(entry.data[key])} + fill={Colors[index % Colors.length]} + stackId={type === 'stacked' ? 'a' : undefined} + name={key} + /> + ); + })} + + + ); +}; diff --git a/ui/src/dashboard/Entry/DashboardEntry.tsx b/ui/src/dashboard/Entry/DashboardEntry.tsx new file mode 100644 index 0000000..7653c53 --- /dev/null +++ b/ui/src/dashboard/Entry/DashboardEntry.tsx @@ -0,0 +1,99 @@ +import {Dashboards_dashboards_items} from '../../gql/__generated__/Dashboards'; +import * as React from 'react'; +import {DashboardPieChart} from './DashboardPieChart'; +import {Stats_stats_entries} from '../../gql/__generated__/Stats'; +import {useQuery} from '@apollo/react-hooks'; +import * as gqlStats from '../../gql/statistics'; +import {Paper} from '@material-ui/core'; +import Typography from '@material-ui/core/Typography'; +import moment from 'moment-timezone'; +import {EntryType} from '../../gql/__generated__/globalTypes'; +import {expectNever} from '../../utils/never'; +import {Stats2, Stats2Variables} from '../../gql/__generated__/Stats2'; +import {DashboardBarChart} from './DashboardBarChart'; +import {DashboardLineChart} from './DashboardLineChart'; +import {CenteredSpinner} from '../../common/CenteredSpinner'; +import {Center} from '../../common/Center'; +import {findRange, Range} from '../../utils/range'; +import {DashboardTable} from './DashboardTable'; + +interface DashboardEntryProps { + entry: Dashboards_dashboards_items; + ranges: Record; + ref?: React.Ref; +} + +export const DashboardEntry: React.FC = React.forwardRef<{}, DashboardEntryProps>(({entry, ranges}, ref) => { + const range = findRange(entry.statsSelection, ranges); + return ( + + + {entry.title} + + + {range.from} to {range.to} + +
+ +
+
+ ); +}); + +// tslint:disable-next-line:cyclomatic-complexity mccabe-complexity +const SpecificDashboardEntry: React.FC<{entry: Dashboards_dashboards_items; range: Range}> = ({entry, range}) => { + const interval = entry.statsSelection.interval; + const stats = useQuery(gqlStats.Stats2, { + variables: { + now: moment() + .startOf('hour') + .format(), + stats: { + range, + interval, + tags: entry.statsSelection.tags, + excludeTags: entry.statsSelection.excludeTags, + includeTags: entry.statsSelection.includeTags, + }, + }, + }); + + if (stats.loading) { + return ; + } + + if (stats.error) { + return ( +
+ error: {stats.error.message} +
+ ); + } + const firstEntries: Stats_stats_entries[] = + (stats.data && stats.data.stats && stats.data.stats[0] && stats.data.stats[0].entries) || []; + if (firstEntries.length === 0) { + return ( +
+ no data +
+ ); + } + + const entries = (stats.data && stats.data.stats) || []; + switch (entry.entryType) { + case EntryType.PieChart: + return ; + case EntryType.BarChart: + return ; + case EntryType.StackedBarChart: + return ; + case EntryType.LineChart: + return ; + case EntryType.VerticalTable: + return ; + case EntryType.HorizontalTable: + return ; + default: + return expectNever(entry.entryType); + } +}; diff --git a/ui/src/dashboard/Entry/DashboardEntryForm.tsx b/ui/src/dashboard/Entry/DashboardEntryForm.tsx new file mode 100644 index 0000000..19d3f2e --- /dev/null +++ b/ui/src/dashboard/Entry/DashboardEntryForm.tsx @@ -0,0 +1,256 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import FormControl from '@material-ui/core/FormControl'; +import InputLabel from '@material-ui/core/InputLabel'; +import Select from '@material-ui/core/NativeSelect/NativeSelect'; +import {useQuery} from 'react-apollo'; +import {Tags} from '../../gql/__generated__/Tags'; +import * as gqlTags from '../../gql/tags'; +import {EntryType, StatsInterval} from '../../gql/__generated__/globalTypes'; +import {toTagSelectorEntry} from '../../tag/tagSelectorEntry'; +import {FormTagSelector} from '../../tag/FormTagSelector'; +import {Dashboards_dashboards_items, Dashboards_dashboards_items_statsSelection_range} from '../../gql/__generated__/Dashboards'; +import {RelativeDateTimeSelector} from '../../common/RelativeDateTimeSelector'; +import {parseRelativeTime} from '../../utils/time'; +import {Grid, Typography, Switch} from '@material-ui/core'; + +interface EditPopupProps { + entry: Dashboards_dashboards_items; + onChange: (entry: Dashboards_dashboards_items | null) => void; + disabled?: boolean; + ranges: Record; +} + +export const isValidDashboardEntry = (item: Dashboards_dashboards_items): boolean => { + return ( + item.statsSelection.tags !== null && + item.statsSelection.tags.length > 0 && + ((item.statsSelection.range && + parseRelativeTime(item.statsSelection.range.from, 'startOf').success && + parseRelativeTime(item.statsSelection.range.to, 'startOf').success) || + !!item.statsSelection.rangeId) && + item.title.length > 0 + ); +}; +export const DashboardEntryForm: React.FC = ({entry, onChange: setEntry, disabled = false, ranges}) => { + const [staticRange, setStaticRange] = React.useState(!entry.statsSelection.rangeId); + + const tagsResult = useQuery(gqlTags.Tags); + + let tagKeys; + let excludeTags; + let includeTags; + if (!tagsResult.error && !tagsResult.loading && tagsResult.data && tagsResult.data.tags) { + const keyInputTags = (entry.statsSelection.tags || []).map((key) => ({key, value: ''})); + tagKeys = toTagSelectorEntry(tagsResult.data.tags, keyInputTags); + excludeTags = toTagSelectorEntry(tagsResult.data.tags, entry.statsSelection.excludeTags || []); + includeTags = toTagSelectorEntry(tagsResult.data.tags, entry.statsSelection.includeTags || []); + } + + const range: Dashboards_dashboards_items_statsSelection_range = entry.statsSelection.range + ? entry.statsSelection.range + : { + from: 'now/w', + to: 'now/w', + __typename: 'RelativeOrStaticRange', + }; + + return ( + <> + { + entry.title = e.target.value; + setEntry(entry); + }} + fullWidth + /> + + Type + + + + Interval + + + + + Range: + Global + + { + if (e.target.checked) { + setStaticRange(true); + entry.statsSelection.rangeId = null; + setEntry(entry); + } else { + setStaticRange(false); + + if (entry.statsSelection.range === null) { + entry.statsSelection.range = range; + setEntry(entry); + } + } + }} + /> + + Static + + + {staticRange ? ( + <> + { + entry.statsSelection.rangeId = null; + entry.statsSelection.range = {...range, from: startDate}; + setEntry(entry); + }} + type="startOf" + /> + { + entry.statsSelection.rangeId = null; + entry.statsSelection.range = {...range, to: endDate}; + setEntry(entry); + }} + type="endOf" + /> + + ) : ( + + )} + {entry.entryType !== EntryType.PieChart ? ( + + + {getTotalTitle(entry.entryType)} + + { + entry.total = e.target.checked; + setEntry(entry); + }} + /> + + + + ) : ( + undefined + )} + { + entry.statsSelection.tags = tags.map((tag) => tag.tag.key); + setEntry(entry); + }} + createTags={false} + onlySelectKeys + /> + + { + entry.statsSelection.excludeTags = tags.map((tag) => ({ + key: tag.tag.key, + value: tag.value, + __typename: 'TimeSpanTag', + })); + setEntry(entry); + }} + allowDuplicateKeys + createTags={false} + /> + { + entry.statsSelection.includeTags = tags.map((tag) => ({ + key: tag.tag.key, + value: tag.value, + __typename: 'TimeSpanTag', + })); + setEntry(entry); + }} + allowDuplicateKeys + createTags={false} + /> + + ); +}; + +const getTotalTitle = (entryType: EntryType) => { + switch (entryType) { + case EntryType.HorizontalTable: + return 'Show "Total" row:'; + case EntryType.VerticalTable: + return 'Show "Total" column:'; + case EntryType.BarChart: + case EntryType.StackedBarChart: + case EntryType.LineChart: + return 'Show "Total" in tooltip:'; + default: + return ''; + } +}; diff --git a/ui/src/dashboard/Entry/DashboardLineChart.tsx b/ui/src/dashboard/Entry/DashboardLineChart.tsx new file mode 100644 index 0000000..90ec14c --- /dev/null +++ b/ui/src/dashboard/Entry/DashboardLineChart.tsx @@ -0,0 +1,68 @@ +import {Stats_stats} from '../../gql/__generated__/Stats'; +import {CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import * as React from 'react'; +import {Colors} from './colors'; +import {ofSeconds} from './unit'; +import {ofInterval} from './dateformat'; +import {StatsInterval} from '../../gql/__generated__/globalTypes'; +import moment from 'moment'; +import {TagTooltip} from './TagTooltip'; + +interface DashboardPieChartProps { + entries: Stats_stats[]; + interval: StatsInterval; + total: boolean; +} + +interface Indexed { + start: string; + end: string; + data: Record; +} + +export const DashboardLineChart: React.FC = ({entries, interval, total}) => { + const indexedEntries: Indexed[] = entries + .map((entry) => { + return { + start: entry.start, + end: entry.end, + data: entry.entries!.reduce((all: Record, current) => { + return {...all, [current.key + ':' + current.value]: current.timeSpendInSeconds}; + }, {}), + }; + }) + .sort((left, right) => moment(left.start).diff(right.start)); + const dataMax = indexedEntries.reduce((max, entry) => { + return Math.max( + max, + Object.values(entry.data).reduce((a: number, b: number) => Math.max(a, b), 0), + 0 + ); + }, 0); + const unit = ofSeconds(dataMax); + const dateFormat = ofInterval(interval); + return ( + + + + + } /> + + dateFormat(moment(entry.start))} interval={'preserveStartEnd'} /> + + {indexedEntries[0] && + Object.keys(indexedEntries[0].data).map((key, index) => { + return ( + unit.toUnit(entry.data[key])} + strokeWidth={3} + stroke={Colors[index % Colors.length]} + name={key} + /> + ); + })} + + + ); +}; diff --git a/ui/src/dashboard/Entry/DashboardPieChart.tsx b/ui/src/dashboard/Entry/DashboardPieChart.tsx new file mode 100644 index 0000000..8441904 --- /dev/null +++ b/ui/src/dashboard/Entry/DashboardPieChart.tsx @@ -0,0 +1,58 @@ +import {Stats_stats_entries} from '../../gql/__generated__/Stats'; +import {Cell, Legend, Pie, PieChart, ResponsiveContainer, Tooltip, TooltipProps} from 'recharts'; +import * as React from 'react'; +import {Colors} from './colors'; +import {Typography} from '@material-ui/core'; +import prettyMs from 'pretty-ms'; +import Paper from '@material-ui/core/Paper'; + +interface DashboardPieChartProps { + entries: Stats_stats_entries[]; +} + +export const DashboardPieChart: React.FC = ({entries}) => { + const total = entries.map((e) => e.timeSpendInSeconds).reduce((x, y) => x + y, 0); + return ( + + + { + // tslint:disable-next-line:no-any + return (entry.key + ':' + entry.value) as any; + }} + data={entries} + labelLine={false} + fill="#8884d8" + legendType={'square'}> + {entries.map((_, index) => ( + + ))} + + } /> + + + + ); +}; + +interface CustomTooltipProps extends TooltipProps { + total: number; +} + +const CustomTooltip = ({active, payload, total}: CustomTooltipProps) => { + if (active && payload) { + const first = payload[0]; + return ( + + + {first.payload.key}:{first.payload.value}: {prettyMs(first.payload.timeSpendInSeconds * 1000)} ( + {total > 0 ? ((first.payload.timeSpendInSeconds / total) * 100).toFixed(2) : '0.00'}%) + + + ); + } + + return null; +}; diff --git a/ui/src/dashboard/Entry/DashboardTable.tsx b/ui/src/dashboard/Entry/DashboardTable.tsx new file mode 100644 index 0000000..7241324 --- /dev/null +++ b/ui/src/dashboard/Entry/DashboardTable.tsx @@ -0,0 +1,94 @@ +import {Stats_stats} from '../../gql/__generated__/Stats'; +import * as React from 'react'; +import {StatsInterval} from '../../gql/__generated__/globalTypes'; +import moment from 'moment'; +import {Table, TableBody, TableCell, TableRow} from '@material-ui/core'; +import TableHead from '@material-ui/core/TableHead'; +import {ofInterval} from './dateformat'; +import prettyMs from 'pretty-ms'; + +interface DashboardTableProps { + entries: Stats_stats[]; + interval: StatsInterval; + mode: 'vertical' | 'horizontal'; + total: boolean; +} + +interface Indexed { + start: string; + end: string; + data: Record; +} + +export const DashboardTable: React.FC = ({entries, interval, mode, total}) => { + const indexedEntries: Indexed[] = entries + .map((entry) => { + const result = { + start: entry.start, + end: entry.end, + data: entry.entries!.reduce((all: Record, current) => { + return {...all, [current.key + ':' + current.value]: current.timeSpendInSeconds}; + }, {}), + }; + if (total) { + result.data['Total'] = Object.values(result.data).reduce((acc, value) => acc + value, 0); + } + return result; + }) + .sort((left, right) => moment(left.start).diff(right.start)); + const dateFormat = ofInterval(interval); + const keys = Object.keys((indexedEntries[0] && indexedEntries[0].data) || {}); + return ( +
+ + {mode === 'vertical' ? ( + <> + + + Date + {keys.map((key) => { + return {key}; + })} + + + + {indexedEntries.map((entry) => { + return ( + + {dateFormat(moment(entry.start))} + {keys.map((key) => ( + {prettyMs(entry.data[key] * 1000)} + ))} + + ); + })} + + + ) : ( + <> + + + Date + {indexedEntries.map((entry) => { + return {dateFormat(moment(entry.start))}; + })} + + + + {keys.map((key) => { + return ( + + {key} + {indexedEntries.map((entry) => ( + {prettyMs(entry.data[key] * 1000)} + ))} + + ); + })} + + + )} +
+
+ ); +}; diff --git a/ui/src/dashboard/Entry/EditGlass.tsx b/ui/src/dashboard/Entry/EditGlass.tsx new file mode 100644 index 0000000..eddb04d --- /dev/null +++ b/ui/src/dashboard/Entry/EditGlass.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import EditIcon from '@material-ui/icons/Edit'; +import Typography from '@material-ui/core/Typography'; +import {Center} from '../../common/Center'; +import DeleteIcon from '@material-ui/icons/Delete'; +import {Button} from '@material-ui/core'; + +interface EditGlassProps { + doEdit: (elm: HTMLElement) => void; + doDelete: () => void; +} + +export const EditGlass: React.FC = ({doEdit, doDelete}) => { + const [confirm, askConfirmation] = React.useState(false); + return ( +
+ {confirm ? ( +
+ Confirm Deletion + + +
+ ) : ( + <> +
+
doEdit(e.currentTarget)}> +
+ +
+
+
askConfirmation(true)}> +
+ +
+
+
+
+
+
+ + drag here + + + resize on corner + +
+
+
+ + )} +
+ ); +}; diff --git a/ui/src/dashboard/Entry/EditPopup.tsx b/ui/src/dashboard/Entry/EditPopup.tsx new file mode 100644 index 0000000..6519a4a --- /dev/null +++ b/ui/src/dashboard/Entry/EditPopup.tsx @@ -0,0 +1,99 @@ +import * as React from 'react'; +import Popper from '@material-ui/core/Popper'; +import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import {Paper} from '@material-ui/core'; +import Typography from '@material-ui/core/Typography'; +import Button from '@material-ui/core/Button'; +import {Dashboards_dashboards_items} from '../../gql/__generated__/Dashboards'; +import {useMutation} from '@apollo/react-hooks'; +import * as gqlDashboard from '../../gql/dashboard'; +import {UpdateDashboardEntry, UpdateDashboardEntryVariables} from '../../gql/__generated__/UpdateDashboardEntry'; +import {Fade} from '../../common/Fade'; +import {DashboardEntryForm, isValidDashboardEntry} from './DashboardEntryForm'; +import {handleError} from '../../utils/errors'; +import {useSnackbar} from 'notistack'; + +interface EditPopupProps { + entry: Dashboards_dashboards_items; + anchorEl: HTMLElement; + onChange: (entry: Dashboards_dashboards_items | null) => void; + doPreview: (preview: boolean) => void; + preview: boolean; + ranges: Record; +} +export const EditPopup: React.FC = ({entry, anchorEl, onChange: setEdit, doPreview, preview, ranges}) => { + const [updateEntry] = useMutation(gqlDashboard.UpdateDashboardEntry, { + refetchQueries: [{query: gqlDashboard.Dashboards}], + }); + const valid = isValidDashboardEntry(entry); + + const {enqueueSnackbar} = useSnackbar(); + + return ( + + + setEdit(null)}> + + Edit + +
+ + + +
+
+
+
+
+ ); +}; diff --git a/ui/src/dashboard/Entry/TagTooltip.tsx b/ui/src/dashboard/Entry/TagTooltip.tsx new file mode 100644 index 0000000..4100ee3 --- /dev/null +++ b/ui/src/dashboard/Entry/TagTooltip.tsx @@ -0,0 +1,39 @@ +import {TooltipProps, TooltipPayload} from 'recharts'; +import {FInterval} from './dateformat'; +import Paper from '@material-ui/core/Paper'; +import {Typography} from '@material-ui/core'; +import moment from 'moment-timezone'; +import prettyMs from 'pretty-ms'; +import * as React from 'react'; + +export const TagTooltip = ({active, payload, dateFormat, total}: TooltipProps & {dateFormat: FInterval; total: boolean}) => { + if (active && payload) { + const first = payload[0]; + const start = dateFormat(moment(first.payload.start)); + const end = dateFormat(moment(first.payload.end)); + + return ( + + {first && {start === end ? `${start}` : `${start} - ${end}`}} + {payload.map((entry) => ( + + {entry.name}: {prettyMs((entry.payload.data[entry.name] as number) * 1000)} + + ))} + {total ? ( + + Total: + {prettyMs(sum(payload) * 1000)} + + ) : ( + undefined + )} + + ); + } + + return null; +}; + +const sum = (payload: readonly TooltipPayload[]): number => + payload.reduce((acc, entry) => (acc + entry.payload.data[entry.name]) as number, 0); diff --git a/ui/src/dashboard/Entry/colors.ts b/ui/src/dashboard/Entry/colors.ts new file mode 100644 index 0000000..1799b95 --- /dev/null +++ b/ui/src/dashboard/Entry/colors.ts @@ -0,0 +1 @@ +export const Colors = ['#5DA5DA', '#60BD68', '#B2912F', '#F17CB0', '#FAA43A', '#F15854', '#DECF3F', '#B276B2', '#4D4D4D']; diff --git a/ui/src/dashboard/Entry/dateformat.ts b/ui/src/dashboard/Entry/dateformat.ts new file mode 100644 index 0000000..9cc2c42 --- /dev/null +++ b/ui/src/dashboard/Entry/dateformat.ts @@ -0,0 +1,21 @@ +import {StatsInterval} from '../../gql/__generated__/globalTypes'; +import * as moment from 'moment-timezone'; +import {expectNever} from '../../utils/never'; + +export type FInterval = (date: moment.Moment) => string; + +export const ofInterval = (interval: StatsInterval): FInterval => { + switch (interval) { + case StatsInterval.Weekly: + case StatsInterval.Monthly: + case StatsInterval.Yearly: + return (d) => d.tz('utc').format('l'); + case StatsInterval.Hourly: + return (d) => d.tz('utc').format('lll'); + case StatsInterval.Single: + case StatsInterval.Daily: + return (d) => d.tz('utc').format('dddd') + ', ' + d.format('l'); + default: + return expectNever(interval); + } +}; diff --git a/ui/src/dashboard/Entry/unit.ts b/ui/src/dashboard/Entry/unit.ts new file mode 100644 index 0000000..d9b5145 --- /dev/null +++ b/ui/src/dashboard/Entry/unit.ts @@ -0,0 +1,33 @@ +interface Unit { + toUnit: (seconds: number) => number; + short: string; + name: string; +} + +const Minutes: Unit = { + toUnit: (s) => s / 60, + name: 'minutes', + short: 'm', +}; +const Hours: Unit = { + toUnit: (s) => Minutes.toUnit(s) / 60, + name: 'hours', + short: 'h', +}; +const Days: Unit = { + toUnit: (s) => Hours.toUnit(s) / 24, + name: 'days', + short: 'd', +}; + +export const ofSeconds = (seconds: number): Unit => { + if (seconds < 2 * 60 * 60) { + return Minutes; + } + + if (seconds < 30 * 60 * 60) { + return Hours; + } + + return Days; +}; diff --git a/ui/src/devices/AddDeviceDialog.tsx b/ui/src/devices/AddDeviceDialog.tsx new file mode 100644 index 0000000..39d6104 --- /dev/null +++ b/ui/src/devices/AddDeviceDialog.tsx @@ -0,0 +1,110 @@ +import * as React from 'react'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import * as gqlDevice from '../gql/device'; +import {MutationFetchResult} from 'react-apollo'; +import {useMutation} from '@apollo/react-hooks'; +import {CreateDevice, CreateDeviceVariables} from '../gql/__generated__/CreateDevice'; +import {useSnackbar} from 'notistack'; +import {handleError} from '../utils/errors'; +import {DeviceType} from '../gql/__generated__/globalTypes'; +import {deviceTypeToString} from './typeutils'; +import Select from '@material-ui/core/NativeSelect'; +import FormControl from '@material-ui/core/FormControl'; +import InputLabel from '@material-ui/core/InputLabel'; + +interface AddTagDialogProps { + initialName: string; + open: boolean; + close: () => void; +} + +export const AddDeviceDialog: React.FC = ({close, open, initialName}) => { + const [token, setToken] = React.useState(''); + const [name, setName] = React.useState(initialName); + const [deviceType, setDeviceType] = React.useState(DeviceType.NoExpiry); + const {enqueueSnackbar} = useSnackbar(); + + const [addDevice] = useMutation(gqlDevice.CreateDevice, { + refetchQueries: [{query: gqlDevice.Devices}], + }); + const submit = (e: React.FormEvent) => { + e.preventDefault(); + addDevice({variables: {deviceType, name}}) + .then((result: MutationFetchResult) => { + if (result.data && result.data.device) { + enqueueSnackbar('Client created', {variant: 'success'}); + setToken(result.data.device.token); + } + }) + .catch(handleError('Create Dialog', enqueueSnackbar)); + }; + + return ( + { + if (token === '') { + close(); + } + }} + aria-labelledby="form-dialog-title" + fullWidth> + {token === '' ? ( +
+ Create Device + + + setName(e.target.value)} + /> + + Expires after + + + + + + + +
+ ) : ( + <> + Device Created + + + The device has the following authentication token, copy it and save it somewhere because you cannot + obtain the token after closing this dialog +
{token} +
+
+ + + + + )} +
+ ); +}; diff --git a/ui/src/devices/DevicesPage.tsx b/ui/src/devices/DevicesPage.tsx new file mode 100644 index 0000000..ef707ea --- /dev/null +++ b/ui/src/devices/DevicesPage.tsx @@ -0,0 +1,162 @@ +import * as React from 'react'; +import Paper from '@material-ui/core/Paper'; +import {makeStyles} from '@material-ui/core/styles'; +import {useMutation, useQuery} from '@apollo/react-hooks'; +import * as gqlDevice from '../gql/device'; +import * as gqlUser from '../gql/user'; +import {Devices} from '../gql/__generated__/Devices'; +import {CenteredSpinner} from '../common/CenteredSpinner'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import moment from 'moment-timezone'; +import DeleteIcon from '@material-ui/icons/Delete'; +import EditIcon from '@material-ui/icons/Edit'; +import DoneIcon from '@material-ui/icons/Done'; +import CloseIcon from '@material-ui/icons/Close'; +import IconButton from '@material-ui/core/IconButton'; +import {RemoveDevice, RemoveDeviceVariables} from '../gql/__generated__/RemoveDevice'; +import {UpdateDevice, UpdateDeviceVariables} from '../gql/__generated__/UpdateDevice'; +import {useSnackbar} from 'notistack'; +import {TextField} from '@material-ui/core'; +import Button from '@material-ui/core/Button'; +import {AddDeviceDialog} from './AddDeviceDialog'; +import {DeviceType} from '../gql/__generated__/globalTypes'; +import {deviceTypeToString} from './typeutils'; +import Select from '@material-ui/core/NativeSelect'; + +const useStyles = makeStyles((theme) => ({ + root: { + ...theme.mixins.gutters(), + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(3), + textAlign: 'center', + maxWidth: 1200, + margin: '0 auto', + }, +})); + +export const DevicesPage = () => { + const classes = useStyles(); + const {data, loading} = useQuery(gqlDevice.Devices); + const refetch = {refetchQueries: [{query: gqlDevice.Devices}, {query: gqlUser.CurrentUser}]}; + const {enqueueSnackbar} = useSnackbar(); + const [removeDevice] = useMutation(gqlDevice.RemoveDevice, refetch); + const [[editId, editName, editDeviceType], setEditing] = React.useState<[number, string, DeviceType]>([ + -1, + '', + DeviceType.NoExpiry, + ]); + const [addActive, setAddActive] = React.useState(false); + const [updateDevice] = useMutation(gqlDevice.UpdateDevice, refetch); + if (loading || !data || !data.currentDevice || !data.devices) { + return ; + } + + const devices = data.devices.map((device) => { + const onClickDelete = () => + removeDevice({variables: {id: device.id}}).then(() => enqueueSnackbar('device deleted', {variant: 'success'})); + const onClickSubmit = () => { + setEditing([-1, '', DeviceType.NoExpiry]); + updateDevice({ + variables: { + id: editId, + name: editName, + deviceType: editDeviceType, + }, + }).then(() => enqueueSnackbar('device edited', {variant: 'success'})); + }; + const isCurrent = device.id === data.currentDevice!.id; + const isEdited = editId === device.id; + return ( + + {device.id} + + {isEdited ? ( + setEditing([editId, e.target.value, editDeviceType])} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onClickSubmit(); + } + }} + onSubmit={onClickSubmit} + style={{minWidth: 128}} + /> + ) : ( + device.name + (isCurrent ? ' (current)' : '') + )} + + {moment(device.createdAt).fromNow()} + + {isEdited ? ( + + ) : ( + deviceTypeToString(device.type) + )} + + {moment(device.activeAt).fromNow()} + + {isEdited ? ( + <> + + + + setEditing([-1, '', DeviceType.NoExpiry])} title="Cancel"> + + + + ) : ( + <> + setEditing([device.id, device.name, device.type])} title="Edit"> + + + + + + + )} + + + ); + }); + + return ( + + + + + + ID + Name + Created + Expires after + Last Active + + + + + {addActive ? setAddActive(false)} /> : null} + {devices} + +
+
+ ); +}; diff --git a/ui/src/devices/typeutils.ts b/ui/src/devices/typeutils.ts new file mode 100644 index 0000000..8527f6f --- /dev/null +++ b/ui/src/devices/typeutils.ts @@ -0,0 +1,15 @@ +import {DeviceType} from '../gql/__generated__/globalTypes'; +import {expectNever} from '../utils/never'; + +export const deviceTypeToString = (type: DeviceType) => { + switch (type) { + case DeviceType.LongExpiry: + return 'A month of inactivity'; + case DeviceType.ShortExpiry: + return 'An hour of inactivity'; + case DeviceType.NoExpiry: + return 'Never'; + default: + return expectNever(type); + } +}; diff --git a/ui/src/global.css b/ui/src/global.css new file mode 100644 index 0000000..fd43541 --- /dev/null +++ b/ui/src/global.css @@ -0,0 +1,37 @@ +#root, +body, +html { + height: 100%; +} + +/* Allows the tag Chips to wrap lines */ +.MuiChip-label { + white-space: normal !important; +} + +/* The wrapping box around the input and icon for the datetime picker */ +.time-picker .MuiInputBase-root.MuiInput-root.MuiInputBase-formControl.MuiInput-formControl.MuiInputBase-adornedEnd { + margin-top: 0; +} + +/* The calendar icon for the datetime picker */ +.time-picker + .MuiInputBase-root.MuiInputBase-formControl.MuiInputBase-adornedEnd + .MuiInputAdornment-root.MuiInputAdornment-positionEnd + .MuiButtonBase-root.MuiIconButton-root { + position: absolute; + left: 0; +} + +/* The label for the datetime picker */ +.time-picker.MuiFormControl-root.MuiTextField-root .MuiFormLabel-root.MuiInputLabel-root.MuiInputLabel-formControl { + margin-left: 48px; +} + +/* The input box for the datetime picker */ +.time-picker.MuiFormControl-root.MuiTextField-root .MuiInputBase-input.MuiInput-input.MuiInputBase-inputAdornedEnd { + margin-left: 48px; + /* margin-top: 8px; */ + margin-top: 12px; + min-width: calc(100% - 48px); +} diff --git a/ui/src/gql/dashboard.ts b/ui/src/gql/dashboard.ts new file mode 100644 index 0000000..a50f0ee --- /dev/null +++ b/ui/src/gql/dashboard.ts @@ -0,0 +1,157 @@ +import {gql} from 'apollo-boost'; + +export const Dashboards = gql` + query Dashboards { + dashboards { + id + name + ranges { + id + name + editable + range { + from + to + } + } + items { + id + title + total + entryType + statsSelection { + range { + from + to + } + rangeId + interval + tags + excludeTags { + key + value + } + includeTags { + key + value + } + } + pos { + mobile { + w + h + x + y + minH + minW + } + desktop { + w + h + x + y + minH + minW + } + } + } + } + } +`; + +export const RemoveDashboard = gql` + mutation RemoveDashboard($id: Int!) { + removeDashboard(id: $id) { + id + } + } +`; + +export const UpdateDashboard = gql` + mutation UpdateDashboard($id: Int!, $name: String!) { + updateDashboard(id: $id, name: $name) { + id + } + } +`; +export const CreateDashboard = gql` + mutation CreateDashboard($name: String!) { + createDashboard(name: $name) { + id + } + } +`; + +export const UpdatePos = gql` + mutation UpdatePos($entryId: Int!, $pos: InputResponsiveDashboardEntryPos!) { + updateDashboardEntry(entryId: $entryId, pos: $pos) { + id + } + } +`; + +export const UpdateDashboardEntry = gql` + mutation UpdateDashboardEntry( + $entryId: Int! + $entryType: EntryType! + $title: String! + $total: Boolean! + $stats: InputStatsSelection! + ) { + updateDashboardEntry(entryId: $entryId, entryType: $entryType, title: $title, total: $total, stats: $stats) { + id + } + } +`; +export const AddDashboardEntry = gql` + mutation AddDashboardEntry( + $dashboardId: Int! + $entryType: EntryType! + $title: String! + $total: Boolean! + $stats: InputStatsSelection! + $pos: InputResponsiveDashboardEntryPos + ) { + addDashboardEntry( + dashboardId: $dashboardId + entryType: $entryType + title: $title + total: $total + stats: $stats + pos: $pos + ) { + id + } + } +`; +export const RemoveDashboardEntry = gql` + mutation RemoveDashboardEntry($id: Int!) { + removeDashboardEntry(id: $id) { + id + } + } +`; + +export const RemoveDashboardRange = gql` + mutation RemoveDashboardRange($rangeId: Int!) { + removeDashboardRange(rangeId: $rangeId) { + id + } + } +`; + +export const UpdateDashboardRange = gql` + mutation UpdateDashboardRange($rangeId: Int!, $range: InputNamedDateRange!) { + updateDashboardRange(rangeId: $rangeId, range: $range) { + name + } + } +`; + +export const AddDashboardRange = gql` + mutation AddDashboardRange($dashboardId: Int!, $range: InputNamedDateRange!) { + addDashboardRange(dashboardId: $dashboardId, range: $range) { + name + } + } +`; diff --git a/ui/src/gql/device.ts b/ui/src/gql/device.ts new file mode 100644 index 0000000..1a6fed9 --- /dev/null +++ b/ui/src/gql/device.ts @@ -0,0 +1,38 @@ +import {gql} from 'apollo-boost'; + +export const Devices = gql` + query Devices { + devices { + id + name + type + createdAt + activeAt + } + currentDevice { + id + } + } +`; +export const RemoveDevice = gql` + mutation RemoveDevice($id: Int!) { + removeDevice(id: $id) { + id + } + } +`; +export const UpdateDevice = gql` + mutation UpdateDevice($id: Int!, $name: String!, $deviceType: DeviceType!) { + updateDevice(id: $id, name: $name, type: $deviceType) { + id + } + } +`; + +export const CreateDevice = gql` + mutation CreateDevice($name: String!, $deviceType: DeviceType!) { + device: createDevice(name: $name, type: $deviceType) { + token + } + } +`; diff --git a/ui/src/gql/settings.ts b/ui/src/gql/settings.ts new file mode 100644 index 0000000..87d9d8d --- /dev/null +++ b/ui/src/gql/settings.ts @@ -0,0 +1,41 @@ +import {gql} from 'apollo-boost'; +import {useQuery} from '@apollo/react-hooks'; +import {Settings as SettingsQueryResponse} from './__generated__/Settings'; +import {DateLocale, Theme, WeekDay, DateTimeInputStyle} from './__generated__/globalTypes'; +import {stripTypename} from '../utils/strip'; + +export const Settings = gql` + query Settings { + userSettings { + theme + dateLocale + firstDayOfTheWeek + dateTimeInputStyle + } + } +`; + +export const SetSettings = gql` + mutation SetSettings($settings: InputUserSettings!) { + setUserSettings(settings: $settings) { + theme + dateTimeInputStyle + } + } +`; + +const defaultSettings = { + theme: Theme.GruvboxDark, + dateLocale: DateLocale.American, + firstDayOfTheWeek: WeekDay.Monday, + dateTimeInputStyle: DateTimeInputStyle.Fancy, +} as const; + +export const useSettings = (): {done: boolean} & Omit => { + const data = useQuery(Settings); + + if (data.loading || data.error || !data.data) { + return {...defaultSettings, done: false}; + } + return {done: true, ...stripTypename(data.data.userSettings)}; +}; diff --git a/ui/src/gql/statistics.ts b/ui/src/gql/statistics.ts new file mode 100644 index 0000000..4599b31 --- /dev/null +++ b/ui/src/gql/statistics.ts @@ -0,0 +1,28 @@ +import {gql} from 'apollo-boost'; + +export const Stats = gql` + query Stats($ranges: [Range!], $tags: [String!], $excludeTags: [InputTimeSpanTag!], $requireTags: [InputTimeSpanTag!]) { + stats(ranges: $ranges, tags: $tags, excludeTags: $excludeTags, requireTags: $requireTags) { + start + end + entries { + key + value + timeSpendInSeconds + } + } + } +`; +export const Stats2 = gql` + query Stats2($now: Time!, $stats: InputStatsSelection!) { + stats: stats2(now: $now, stats: $stats) { + start + end + entries { + key + value + timeSpendInSeconds + } + } + } +`; diff --git a/ui/src/gql/tags.ts b/ui/src/gql/tags.ts new file mode 100644 index 0000000..67682bd --- /dev/null +++ b/ui/src/gql/tags.ts @@ -0,0 +1,52 @@ +import {gql} from 'apollo-boost'; + +export const SuggestTag = gql` + query SuggestTag($query: String!) { + tags: suggestTag(query: $query) { + color + key + } + } +`; + +export const Tags = gql` + query Tags { + tags { + color + key + usages + } + } +`; +export const SuggestTagValue = gql` + query SuggestTagValue($tag: String!, $query: String!) { + values: suggestTagValue(key: $tag, query: $query) + } +`; + +export const AddTag = gql` + mutation AddTag($name: String!, $color: String!) { + createTag(key: $name, color: $color) { + color + key + } + } +`; + +export const UpdateTag = gql` + mutation UpdateTag($key: String!, $newKey: String, $color: String!) { + updateTag(key: $key, newKey: $newKey, color: $color) { + color + key + } + } +`; + +export const RemoveTag = gql` + mutation RemoveTag($key: String!) { + removeTag(key: $key) { + color + key + } + } +`; diff --git a/ui/src/gql/timeSpan.ts b/ui/src/gql/timeSpan.ts new file mode 100644 index 0000000..2307b79 --- /dev/null +++ b/ui/src/gql/timeSpan.ts @@ -0,0 +1,134 @@ +import {gql} from 'apollo-boost'; + +export const Trackers = gql` + query Trackers { + timers { + id + start + end + tags { + key + value + } + oldStart + note + } + } +`; + +export const TimeSpansInRange = gql` + query TimeSpansInRange($start: Time!, $end: Time!) { + timeSpans(fromInclusive: $start, toInclusive: $end) { + timeSpans { + id + start + end + tags { + key + value + } + oldStart + note + } + cursor { + startId + offset + pageSize + } + } + } +`; + +export const TimeSpans = gql` + query TimeSpans($cursor: InputCursor) { + timeSpans(cursor: $cursor) @connection(key: "AllTimeSpans") { + timeSpans { + id + start + end + tags { + key + value + } + oldStart + note + } + cursor { + startId + offset + pageSize + } + } + } +`; + +export const StartTimer = gql` + mutation StartTimer($start: Time!, $tags: [InputTimeSpanTag!], $note: String!) { + createTimeSpan(start: $start, tags: $tags, note: $note) { + id + start + end + tags { + key + value + } + oldStart + note + } + } +`; + +export const StopTimer = gql` + mutation StopTimer($id: Int!, $end: Time!) { + stopTimeSpan(id: $id, end: $end) { + id + start + end + tags { + key + value + } + oldStart + note + } + } +`; + +export const AddTimeSpan = gql` + mutation AddTimeSpan($start: Time!, $end: Time!, $tags: [InputTimeSpanTag!], $note: String!) { + createTimeSpan(start: $start, end: $end, tags: $tags, note: $note) { + id + start + end + tags { + key + value + } + oldStart + note + } + } +`; + +export const UpdateTimeSpan = gql` + mutation UpdateTimeSpan($id: Int!, $start: Time!, $end: Time, $tags: [InputTimeSpanTag!], $oldStart: Time, $note: String!) { + updateTimeSpan(id: $id, start: $start, end: $end, tags: $tags, oldStart: $oldStart, note: $note) { + id + start + end + tags { + key + value + } + oldStart + note + } + } +`; +export const RemoveTimeSpan = gql` + mutation RemoveTimeSpan($id: Int!) { + removeTimeSpan(id: $id) { + id + } + } +`; diff --git a/ui/src/gql/user.ts b/ui/src/gql/user.ts new file mode 100644 index 0000000..d4647c0 --- /dev/null +++ b/ui/src/gql/user.ts @@ -0,0 +1,72 @@ +import {gql} from 'apollo-boost'; + +export const CurrentUser = gql` + query CurrentUser { + user: currentUser { + name + admin + id + } + } +`; + +export const Login = gql` + mutation Login($name: String!, $pass: String!, $deviceType: DeviceType!) { + login(username: $name, pass: $pass, deviceName: "web ui", type: $deviceType, cookie: true) { + user { + id + name + admin + } + } + } +`; +export const Logout = gql` + mutation Logout { + user: removeCurrentDevice { + name + } + } +`; + +export const Users = gql` + query Users { + users { + id + name + admin + } + currentUser { + id + } + } +`; + +export const RemoveUser = gql` + mutation RemoveUser($id: Int!) { + removeUser(id: $id) { + id + name + admin + } + } +`; + +export const UpdateUser = gql` + mutation UpdateUser($id: Int!, $name: String!, $admin: Boolean!, $pass: String) { + updateUser(id: $id, name: $name, admin: $admin, pass: $pass) { + id + name + admin + } + } +`; +export const CreateUser = gql` + mutation CreateUser($name: String!, $admin: Boolean!, $pass: String!) { + createUser(name: $name, admin: $admin, pass: $pass) { + id + name + admin + } + } +`; diff --git a/ui/src/gql/utils.ts b/ui/src/gql/utils.ts new file mode 100644 index 0000000..9185eb4 --- /dev/null +++ b/ui/src/gql/utils.ts @@ -0,0 +1,79 @@ +import {DataProxy} from 'apollo-cache'; +import {TimeSpans} from './__generated__/TimeSpans'; +import * as gqlTimeSpan from './timeSpan'; +import moment from 'moment'; +import {AddTimeSpan_createTimeSpan} from './__generated__/AddTimeSpan'; +import {TimeSpansInRange, TimeSpansInRangeVariables} from './__generated__/TimeSpansInRange'; +import {Trackers} from './__generated__/Trackers'; +import {StopTimer} from './__generated__/StopTimer'; + +export const addTimeSpanToCache = (cache: DataProxy, ts: AddTimeSpan_createTimeSpan) => { + let oldTimeSpans: TimeSpans | null = null; + try { + oldTimeSpans = cache.readQuery({query: gqlTimeSpan.TimeSpans}); + } catch {} + if (!oldTimeSpans) { + return; + } + cache.writeQuery({ + query: gqlTimeSpan.TimeSpans, + data: { + timeSpans: { + __typename: 'PagedTimeSpans', + timeSpans: oldTimeSpans.timeSpans.timeSpans + .concat([ts]) + .sort((a, b) => moment(b.start).unix() - moment(a.start).unix()), + cursor: oldTimeSpans.timeSpans.cursor, + }, + }, + }); +}; +export const addTimeSpanInRangeToCache = (cache: DataProxy, ts: AddTimeSpan_createTimeSpan, vars: TimeSpansInRangeVariables) => { + const oldTimeSpans = cache.readQuery({query: gqlTimeSpan.TimeSpansInRange, variables: vars}); + if (!oldTimeSpans) { + return; + } + cache.writeQuery({ + query: gqlTimeSpan.TimeSpansInRange, + variables: vars, + data: { + timeSpans: { + __typename: 'PagedTimeSpans', + timeSpans: oldTimeSpans.timeSpans.timeSpans + .concat([ts]) + .sort((a, b) => moment(b.start).unix() - moment(a.start).unix()), + cursor: oldTimeSpans.timeSpans.cursor, + }, + }, + }); +}; +export const removeFromTrackersCache = (cache: DataProxy, data: StopTimer) => { + const oldTrackers = cache.readQuery({query: gqlTimeSpan.Trackers}); + if (!oldTrackers || !data || !data.stopTimeSpan) { + return; + } + cache.writeQuery({ + query: gqlTimeSpan.Trackers, + data: { + timers: (oldTrackers.timers || []).filter((tracker) => tracker.id !== data.stopTimeSpan!.id), + }, + }); +}; + +export const removeFromTimeSpanInRangeCache = (cache: DataProxy, id: number, vars: TimeSpansInRangeVariables) => { + const old = cache.readQuery({query: gqlTimeSpan.TimeSpansInRange, variables: vars}); + if (!old) { + return; + } + cache.writeQuery({ + query: gqlTimeSpan.TimeSpansInRange, + variables: vars, + data: { + timeSpans: { + __typename: 'PagedTimeSpans', + timeSpans: old.timeSpans.timeSpans.filter((ts) => ts.id !== id), + cursor: old.timeSpans.cursor, + }, + }, + }); +}; diff --git a/ui/src/gql/version.ts b/ui/src/gql/version.ts new file mode 100644 index 0000000..05493dd --- /dev/null +++ b/ui/src/gql/version.ts @@ -0,0 +1,16 @@ +import {gql} from 'apollo-boost'; +import {Version as VersionResponse} from './__generated__/Version'; + +export const Version = gql` + query Version { + version { + name + commit + buildDate + } + } +`; + +export const VersionDefault: VersionResponse = { + version: {__typename: 'Version', commit: 'unknown', buildDate: 'unknown', name: 'vUnknown'}, +}; diff --git a/ui/src/index.tsx b/ui/src/index.tsx new file mode 100644 index 0000000..95809fc --- /dev/null +++ b/ui/src/index.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {Root} from './Root'; +import 'moment/locale/de'; +import 'moment/locale/en-au'; +import 'moment/locale/en-gb'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/ui/src/login/LoginForm.tsx b/ui/src/login/LoginForm.tsx new file mode 100644 index 0000000..4ef6e42 --- /dev/null +++ b/ui/src/login/LoginForm.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import Button from '@material-ui/core/Button'; +import {useSnackbar} from 'notistack'; +import {handleError} from '../utils/errors'; +import * as gqlUser from '../gql/user'; +import {Login, LoginVariables} from '../gql/__generated__/Login'; +import {useMutation} from '@apollo/react-hooks'; +import {Checkbox} from '@material-ui/core'; +import {DeviceType} from '../gql/__generated__/globalTypes'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import makeStyles from '@material-ui/core/styles/makeStyles'; +import {Settings as SettingsGQL} from '../gql/settings'; + +const useStyles = makeStyles((theme) => ({ + button: { + marginTop: theme.spacing(1), + }, +})); + +export const LoginForm = () => { + const classes = useStyles(); + const [login] = useMutation(gqlUser.Login, { + update: (cache, {data}) => { + cache.writeQuery({query: gqlUser.CurrentUser, data: {user: data && data.login && data.login.user}}); + }, + refetchQueries: [{query: SettingsGQL}], + }); + + const {enqueueSnackbar} = useSnackbar(); + const [username, setUsername] = React.useState(''); + const [password, setPassword] = React.useState(''); + const [remember, setRemember] = React.useState(false); + const submit = (e: React.FormEvent) => { + e.preventDefault(); + login({ + variables: { + name: username, + pass: password, + deviceType: remember ? DeviceType.LongExpiry : DeviceType.ShortExpiry, + }, + }) + .then(() => enqueueSnackbar('Login successful', {variant: 'success'})) + .catch(handleError('Login failed', enqueueSnackbar)); + }; + return ( +
+ setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + setRemember(e.target.checked)} />} + label="Remember Me" + /> + + + ); +}; diff --git a/ui/src/login/LoginPage.tsx b/ui/src/login/LoginPage.tsx new file mode 100644 index 0000000..6ce4cce --- /dev/null +++ b/ui/src/login/LoginPage.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import Typography from '@material-ui/core/Typography'; +import Grid from '@material-ui/core/Grid'; +import {LoginForm} from './LoginForm'; +import Link from '@material-ui/core/Link'; +import {DefaultPaper} from '../common/DefaultPaper'; +import * as gqlVersion from '../gql/version'; +import {useQuery} from '@apollo/react-hooks'; +import {Version} from '../gql/__generated__/Version'; +import makeStyles from '@material-ui/core/styles/makeStyles'; + +const useStyles = makeStyles(() => ({ + footerLink: { + margin: '0 2px', + }, +})); + +export const LoginPage = () => { + const classes = useStyles(); + const {data: {version = gqlVersion.VersionDefault.version} = gqlVersion.VersionDefault} = useQuery( + gqlVersion.Version + ); + return ( + + + + + traggo + + + + +
+ + Source Code + + + | + + + Bug Tracker + + + | + + + {version.name}@{version.commit.slice(0, 8)} + +
+
+ ); +}; diff --git a/ui/src/provider/ApolloProvider.tsx b/ui/src/provider/ApolloProvider.tsx new file mode 100644 index 0000000..4b11bdf --- /dev/null +++ b/ui/src/provider/ApolloProvider.tsx @@ -0,0 +1,16 @@ +import ApolloClient from 'apollo-boost'; +import * as React from 'react'; +import {ApolloProvider as Provider} from 'react-apollo'; +import {ApolloProvider as ApolloProviderHooks} from '@apollo/react-hooks'; + +const client = new ApolloClient({ + uri: './graphql', +}); + +export const ApolloProvider: React.FC = ({children}) => { + return ( + + {children} + + ); +}; diff --git a/ui/src/provider/SnackbarProvider.tsx b/ui/src/provider/SnackbarProvider.tsx new file mode 100644 index 0000000..629ab3e --- /dev/null +++ b/ui/src/provider/SnackbarProvider.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import {SnackbarProvider as Provider} from 'notistack'; +import {makeStyles} from '@material-ui/core/styles'; + +const useStyles = makeStyles(() => ({ + error: { + background: '#E53935', + color: '#fff', + }, + warning: { + background: '#d35400', + color: '#fff', + }, + info: { + background: '#2980b9', + color: '#fff', + }, +})); + +export const SnackbarProvider: React.FC = ({children}) => { + const classes = useStyles(); + return ( + + {children} + + ); +}; diff --git a/ui/src/provider/ThemeProvider.tsx b/ui/src/provider/ThemeProvider.tsx new file mode 100644 index 0000000..678ca10 --- /dev/null +++ b/ui/src/provider/ThemeProvider.tsx @@ -0,0 +1,141 @@ +import * as React from 'react'; +import {MuiThemeProvider, Theme} from '@material-ui/core/styles'; +import createMuiTheme from '@material-ui/core/styles/createMuiTheme'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import {useSettings} from '../gql/settings'; +import {Theme as SettingTheme} from '../gql/__generated__/globalTypes'; + +const themes: Record = { + [SettingTheme.GruvboxDark]: createMuiTheme({ + overrides: { + MuiLink: { + root: { + color: '#458588', + }, + }, + MuiIconButton: { + root: { + color: 'inherit', + }, + }, + MuiListItemIcon: { + root: { + color: 'inherit', + }, + }, + MuiToolbar: { + root: { + background: '#a89984', + }, + }, + }, + palette: { + background: { + default: '#282828', + paper: '#32302f', + }, + text: { + primary: '#fbf1d4', + }, + primary: { + main: '#a89984', + }, + secondary: { + main: '#f44336', + }, + type: 'dark', + }, + }), + [SettingTheme.GruvboxLight]: createMuiTheme({ + overrides: { + MuiLink: { + root: { + color: '#458588', + }, + }, + MuiIconButton: { + root: { + color: 'inherit', + }, + }, + MuiListItemIcon: { + root: { + color: 'inherit', + }, + }, + MuiToolbar: { + root: { + background: '#7c6f64', + }, + }, + }, + palette: { + background: { + default: '#fbf1c7', + paper: '#f9f5d7', + }, + text: { + primary: '#282828', + }, + primary: { + main: '#7c6f64', + }, + secondary: { + main: '#f44336', + }, + type: 'light', + }, + }), + [SettingTheme.MaterialLight]: createMuiTheme({ + overrides: { + MuiLink: { + root: { + color: '#2980b9', + }, + }, + MuiIconButton: { + root: { + color: 'inherit', + }, + }, + }, + palette: { + background: {default: '#eeeeee'}, + primary: { + main: '#455a64', + }, + secondary: { + main: '#f44336', + }, + type: 'light', + }, + }), + [SettingTheme.MaterialDark]: createMuiTheme({ + overrides: { + MuiLink: { + root: { + color: '#3498db', + }, + }, + }, + palette: { + primary: { + main: '#455a64', + }, + secondary: { + main: '#f44336', + }, + type: 'dark', + }, + }), +}; + +export const ThemeProvider: React.FC = ({children}) => { + const {theme} = useSettings(); + return ( + + + {children} + + ); +}; diff --git a/ui/src/provider/UserSettingsProvider.tsx b/ui/src/provider/UserSettingsProvider.tsx new file mode 100644 index 0000000..9ec7a23 --- /dev/null +++ b/ui/src/provider/UserSettingsProvider.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import {useSettings} from '../gql/settings'; +import {CenteredSpinner} from '../common/CenteredSpinner'; +import moment, {LocaleSpecification} from 'moment'; +import {DateLocale, WeekDay} from '../gql/__generated__/globalTypes'; +import {expectNever} from '../utils/never'; + +const setLocale = (locale: DateLocale, spec: LocaleSpecification) => { + switch (locale) { + case DateLocale.American: + moment.locale('en', spec); + return; + case DateLocale.American24h: + moment.locale('en', { + ...spec, + longDateFormat: { + LTS: 'HH:mm:ss', + LT: 'HH:mm', + L: 'MM/DD/YYYY', + LL: 'MMMM D, YYYY', + LLL: 'MMMM D, YYYY HH:mm', + LLLL: 'dddd, MMMM D, YYYY HH:mm', + }, + }); + return; + case DateLocale.Australian: + moment.locale('en-au', spec); + return; + case DateLocale.British: + moment.locale('en-gb', spec); + return; + case DateLocale.German: + moment.locale('de', spec); + return; + default: + expectNever(locale); + return; + } +}; + +const weekDayToMoment = (s: WeekDay): number => { + switch (s) { + case WeekDay.Sunday: + return 0; + case WeekDay.Monday: + return 1; + case WeekDay.Tuesday: + return 2; + case WeekDay.Wednesday: + return 3; + case WeekDay.Thursday: + return 4; + case WeekDay.Friday: + return 5; + case WeekDay.Saturday: + return 6; + default: + return expectNever(s); + } +}; + +export const BootUserSettings: React.FC = ({children}): React.ReactElement => { + const {done, firstDayOfTheWeek, dateLocale} = useSettings(); + + React.useEffect(() => { + if (!done) { + return; + } + setLocale(dateLocale, { + week: { + dow: weekDayToMoment(firstDayOfTheWeek), + doy: moment.localeData(moment.locale()).firstDayOfYear(), + }, + }); + }, [dateLocale, firstDayOfTheWeek, done]); + + if (!done) { + return ; + } + + return <>{children}; +}; diff --git a/ui/src/react-app-env.d.ts b/ui/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/ui/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ui/src/setting/SettingsPage.tsx b/ui/src/setting/SettingsPage.tsx new file mode 100644 index 0000000..d4b75ba --- /dev/null +++ b/ui/src/setting/SettingsPage.tsx @@ -0,0 +1,153 @@ +import * as React from 'react'; +import makeStyles from '@material-ui/core/styles/makeStyles'; +import {Paper} from '@material-ui/core'; +import {SetSettings as SetSettingsGQL, Settings as SettingsGQL, useSettings} from '../gql/settings'; +import {useMutation} from '@apollo/react-hooks'; +import {SetSettings, SetSettingsVariables} from '../gql/__generated__/SetSettings'; +import FormControl from '@material-ui/core/FormControl'; +import InputLabel from '@material-ui/core/InputLabel'; +import Select from '@material-ui/core/NativeSelect/NativeSelect'; +import {DateLocale, Theme, WeekDay, DateTimeInputStyle} from '../gql/__generated__/globalTypes'; +import {useSnackbar} from 'notistack'; +import {handleError} from '../utils/errors'; + +const useStyles = makeStyles((theme) => ({ + root: { + ...theme.mixins.gutters(), + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(3), + maxWidth: 500, + margin: '0 auto', + }, +})); + +export const SettingsPage: React.FC = () => { + const classes = useStyles(); + const {done, ...settings} = useSettings(); + const {enqueueSnackbar} = useSnackbar(); + const [setSettings] = useMutation(SetSettingsGQL, { + refetchQueries: [{query: SettingsGQL}], + }); + return ( + + + Date Locale + + + + Theme + + + + First day of the week + + + + Datetime input style + + + + ); +}; diff --git a/ui/src/tag/AddTagDialog.tsx b/ui/src/tag/AddTagDialog.tsx new file mode 100644 index 0000000..5a48b78 --- /dev/null +++ b/ui/src/tag/AddTagDialog.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import {SliderPicker} from 'react-color'; +import {InputLabel} from '@material-ui/core'; +import FormControl from '@material-ui/core/FormControl'; +import {MutationFetchResult} from 'react-apollo'; +import {AddTag, AddTagVariables} from '../gql/__generated__/AddTag'; +import * as gqlTags from '../gql/tags'; +import {TagSelectorEntry} from './tagSelectorEntry'; +import {useMutation} from '@apollo/react-hooks'; +import {useSnackbar} from 'notistack'; +import {handleError} from '../utils/errors'; + +interface AddTagDialogProps { + initialName: string; + open: boolean; + close: () => void; + onAdded?: (tag: TagSelectorEntry['tag']) => void; +} + +export const AddTagDialog: React.FC = ({close, open, initialName, onAdded = () => {}}) => { + const [name, setName] = React.useState(initialName); + const [color, setColor] = React.useState('#e6b3b3'); + const {enqueueSnackbar} = useSnackbar(); + + const [addTag] = useMutation(gqlTags.AddTag, {refetchQueries: [{query: gqlTags.Tags}]}); + const submit = (e: React.FormEvent) => { + e.preventDefault(); + addTag({variables: {name, color}}) + .then((result: MutationFetchResult | void) => { + close(); + if (result && result.data && result.data.createTag) { + onAdded(result.data.createTag); + } + }) + .catch(handleError('Add Tag', enqueueSnackbar)); + }; + + return ( + +
+ Create Tag + + + setName(e.target.value)} + /> + + + Color + +
+ setColor(c.hex)} color={color} /> +
+
+
+ + + + +
+
+ ); +}; diff --git a/ui/src/tag/FormTagSelector.tsx b/ui/src/tag/FormTagSelector.tsx new file mode 100644 index 0000000..8113669 --- /dev/null +++ b/ui/src/tag/FormTagSelector.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import FormControl from '@material-ui/core/FormControl'; +import Box from '@material-ui/core/Box'; +import InputLabel from '@material-ui/core/InputLabel'; +import {TagSelector, TagSelectorProps} from './TagSelector'; + +interface FormTagSelectorProps extends TagSelectorProps { + label: string; + required?: boolean; +} + +export const FormTagSelector = ({label, required = false, ...props}: FormTagSelectorProps) => { + return ( + + + + {label} + + + + + + + ); +}; diff --git a/ui/src/tag/TagPage.tsx b/ui/src/tag/TagPage.tsx new file mode 100644 index 0000000..13fe91a --- /dev/null +++ b/ui/src/tag/TagPage.tsx @@ -0,0 +1,161 @@ +import * as React from 'react'; +import Paper from '@material-ui/core/Paper'; +import {makeStyles} from '@material-ui/core/styles'; +import {useMutation, useQuery} from '@apollo/react-hooks'; +import * as gqlTag from '../gql/tags'; +import * as gqlDashboard from '../gql/dashboard'; +import {CenteredSpinner} from '../common/CenteredSpinner'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import DeleteIcon from '@material-ui/icons/Delete'; +import EditIcon from '@material-ui/icons/Edit'; +import DoneIcon from '@material-ui/icons/Done'; +import CloseIcon from '@material-ui/icons/Close'; +import IconButton from '@material-ui/core/IconButton'; +import {useSnackbar} from 'notistack'; +import {TextField} from '@material-ui/core'; +import Button from '@material-ui/core/Button'; +import {Tags} from '../gql/__generated__/Tags'; +import {RemoveTag, RemoveTagVariables} from '../gql/__generated__/RemoveTag'; +import {UpdateTag, UpdateTagVariables} from '../gql/__generated__/UpdateTag'; +import {AddTagDialog} from './AddTagDialog'; +import {SliderPicker} from 'react-color'; +import {TagChip} from '../common/TagChip'; +import {handleError} from '../utils/errors'; +import {ConfirmDialog} from '../common/ConfirmDialog'; + +const useStyles = makeStyles((theme) => ({ + root: { + ...theme.mixins.gutters(), + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(3), + textAlign: 'center', + maxWidth: 1200, + margin: '0 auto', + }, +})); + +export const TagPage = () => { + const classes = useStyles(); + const {data, loading} = useQuery(gqlTag.Tags); + const [removeTagConfirm, setRemoveTagConfirm] = React.useState(''); + const refetch = {refetchQueries: [{query: gqlTag.Tags}, {query: gqlDashboard.Dashboards}]}; + const {enqueueSnackbar} = useSnackbar(); + const [removeTag] = useMutation(gqlTag.RemoveTag, refetch); + const [[editKey, editKeyNew, editColor], setEditing] = React.useState<[string, string, string]>(['', '', '']); + const [addActive, setAddActive] = React.useState(false); + const [updateTag] = useMutation(gqlTag.UpdateTag, refetch); + if (loading || !data || !data.tags) { + return ; + } + const onClickDelete = () => { + return removeTag({variables: {key: removeTagConfirm}}) + .then(() => enqueueSnackbar('tag deleted', {variant: 'success'})) + .catch(handleError('Delete Tag', enqueueSnackbar)); + }; + + const tags = data.tags.map((tag) => { + const onClickSubmit = () => { + setEditing(['', '', '']); + updateTag({ + variables: { + key: editKey, + newKey: editKeyNew, + color: editColor, + }, + }) + .then(() => enqueueSnackbar('tag edited', {variant: 'success'})) + .catch(handleError('Edit Tag', enqueueSnackbar)); + }; + const isEdited = editKey === tag.key; + return ( + + + {isEdited ? ( + setEditing([editKey, e.target.value, editColor])} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onClickSubmit(); + } + }} + onSubmit={onClickSubmit} + style={{minWidth: 128}} + /> + ) : ( + tag.key + )} + + + {isEdited ? ( + setEditing([editKey, editKeyNew, c.hex])} color={editColor} /> + ) : ( + + )} + + {tag.usages} + + {isEdited ? ( + <> + + + + setEditing(['', '', ''])} title="Cancel"> + + + + ) : ( + <> + setEditing([tag.key, tag.key, tag.color])} title="Edit"> + + + setRemoveTagConfirm(tag.key)} title="Delete"> + + + + )} + + + ); + }); + + return ( + + + + + + Key + Color + Usages + + + + + {addActive ? setAddActive(false)} /> : null} + {tags} + {removeTagConfirm ? ( + setRemoveTagConfirm('')} + fOnSubmit={onClickDelete}> + This operation cannot be undone. Deleting the tag will remove all references in time spans. + + ) : null} + +
+
+ ); +}; diff --git a/ui/src/tag/TagSelector.tsx b/ui/src/tag/TagSelector.tsx new file mode 100644 index 0000000..dda8fc6 --- /dev/null +++ b/ui/src/tag/TagSelector.tsx @@ -0,0 +1,279 @@ +import * as React from 'react'; +import {Tags} from '../gql/__generated__/Tags'; +import * as gqlTags from '../gql/tags'; +import {useQuery} from '@apollo/react-hooks'; +import {useError} from '../utils/errors'; +import Typography from '@material-ui/core/Typography'; +import Tooltip from '@material-ui/core/Tooltip'; +import MenuItem from '@material-ui/core/MenuItem'; +import {AddTagDialog} from './AddTagDialog'; +import {TagSelectorEntry, itemLabel, label, addValues} from './tagSelectorEntry'; +import {useSuggest} from './suggest'; +import Paper from '@material-ui/core/Paper'; +import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import Input from '@material-ui/core/Input'; +import {useStateAndDelegateWithDelayOnChange} from '../utils/hooks'; +import {TagChip} from '../common/TagChip'; +import {makeStyles, Theme} from '@material-ui/core/styles'; + +const useStyles = makeStyles((theme: Theme) => ({ + root: { + width: '100%', + }, + inputRoot: {display: 'flex', flexWrap: 'wrap', cursor: 'text', width: '100%'}, + inputInput: {height: 40, minWidth: 150, flexGrow: 1}, + paper: { + position: 'absolute', + zIndex: 1, + marginTop: theme.spacing(1), + }, +})); + +export interface TagSelectorProps { + onSelectedEntriesChanged: (entries: TagSelectorEntry[]) => void; + selectedEntries: TagSelectorEntry[]; + dialogOpen?: React.Dispatch>; + onCtrlEnter?: () => void; + createTags?: boolean; + allowDuplicateKeys?: boolean; + onlySelectKeys?: boolean; +} + +export const TagSelector: React.FC = ({ + selectedEntries, + onSelectedEntriesChanged: setSelectedEntries, + dialogOpen = () => {}, + onCtrlEnter, + createTags = true, + allowDuplicateKeys = false, + onlySelectKeys = false, +}) => { + const classes = useStyles(); + const [tooltipErrorActive, tooltipError, showTooltipError] = useError(4000); + const [open, setOpen] = React.useState(false); + const [currentValue, setCurrentValueInternal] = React.useState(''); + const [highlightedIndex, setHighlightedIndex] = React.useState(0); + const [addDialogOpen, setAddDialogOpen] = useStateAndDelegateWithDelayOnChange(false, dialogOpen); + const input = React.useRef(null); + const container = React.useRef(null); + + const tagsResult = useQuery(gqlTags.Tags); + + const suggestions = useSuggest(tagsResult, currentValue, selectedEntries, onlySelectKeys, allowDuplicateKeys, createTags); + + if (tagsResult.error || tagsResult.loading || !tagsResult.data || !tagsResult.data.tags) { + return null; + } + + const setCurrentValue = (newValue: string) => { + if (currentValue.indexOf(' ') !== -1) { + throw new Error('old value should never contain a space'); + } + setHighlightedIndex(0); + + if (newValue.indexOf(' ') !== -1) { + const {errors, entries} = addValues(newValue, tagsResult, selectedEntries, onlySelectKeys, allowDuplicateKeys); + + setSelectedEntries([...selectedEntries, ...entries]); + + if (errors.length > 0) { + if (errors.length === 1) { + showTooltipError(errors[0].error); + } else { + showTooltipError('skipped tags because:\n' + errors.map((e) => e.error).join('\n')); + } + setCurrentValueInternal(errors[errors.length - 1].value); + } else { + setCurrentValueInternal(''); + } + return; + } + + setCurrentValueInternal(newValue); + }; + + const focusInput = () => { + if (input.current) { + input.current.focus(); + } + }; + + const trySubmit = (entry: TagSelectorEntry) => { + if (entry.tag.alreadyUsed) { + return; + } + if (entry.tag.create) { + setAddDialogOpen(true); + return; + } + + focusInput(); + + if (!onlySelectKeys && !entry.value) { + const newValue = entry.tag.key + ':'; + if (currentValue !== newValue) { + setHighlightedIndex(0); + setCurrentValue(newValue); + } else { + showTooltipError('enter a value after the colon'); + } + return; + } + + setSelectedEntries([...selectedEntries, entry]); + setCurrentValue(''); + return; + }; + + const onTagClicked = (entry: TagSelectorEntry, edit: boolean) => { + // Prevent overwriting text that's already being edited + if (currentValue && edit) { + showTooltipError('Input is not empty. Use ctrl+click to delete tag without editing.'); + return; + } + + setSelectedEntries(selectedEntries.filter((selected) => selected !== entry)); + + if (edit) { + setCurrentValueInternal(itemLabel(entry, onlySelectKeys)); + setOpen(true); + } + + focusInput(); + }; + + const onKeyDown = (event: React.KeyboardEvent) => { + if (!currentValue && selectedEntries.length && event.key === 'Backspace') { + event.preventDefault(); + const last = selectedEntries[selectedEntries.length - 1]; + setSelectedEntries(selectedEntries.slice(0, selectedEntries.length - 1)); + setCurrentValue(event.ctrlKey ? '' : itemLabel(last, onlySelectKeys)); + } + if (event.key === 'ArrowUp') { + event.preventDefault(); + setHighlightedIndex(highlightedIndex === 0 ? suggestions.length - 1 : highlightedIndex - 1); + } + if (event.key === 'ArrowDown') { + event.preventDefault(); + setHighlightedIndex(highlightedIndex === suggestions.length - 1 ? 0 : highlightedIndex + 1); + } + if (event.key === 'Enter' && highlightedIndex < suggestions.length) { + event.preventDefault(); + if (!currentValue && event.ctrlKey && onCtrlEnter) { + onCtrlEnter(); + } else { + trySubmit(suggestions[highlightedIndex]); + } + } + if (event.key === 'Escape' && input.current) { + event.preventDefault(); + setOpen(false); + } + if (event.key === 'Tab') { + setOpen(false); + } + }; + + return ( + setOpen(false)}> +
+ + {tooltipError} + + }> +
(container.current = ref)} className={classes.inputRoot} onClick={focusInput}> + {toChips(selectedEntries, onlySelectKeys, onTagClicked)} + (input.current = ref)} + onFocus={() => setOpen(true)} + onKeyDown={onKeyDown} + disableUnderline={true} + onChange={(e) => setCurrentValue(e.target.value)} + placeholder="Enter Tags" + className={classes.inputInput} + /> +
+
+ + {open ? ( + + {suggestions.map((entry, index) => ( + + ))} + + ) : null} + {addDialogOpen && ( + { + const valuePart = currentValue.split(':')[1] || ''; + trySubmit({tag, value: valuePart}); + }} + open={true} + initialName={currentValue.split(':')[0]} + close={() => { + setTimeout(focusInput, 50); + setAddDialogOpen(false); + }} + /> + )} +
+
+ ); +}; + +interface ItemProps { + entry: TagSelectorEntry; + selected: boolean; + onlySelectKeys: boolean; + onClick: (entry: TagSelectorEntry) => void; +} + +const Item: React.FC = ({entry, selected, onlySelectKeys, onClick}) => { + return ( + onClick(entry)} + disabled={entry.tag.alreadyUsed} + style={{ + fontWeight: selected ? 500 : 400, + }}> + {itemLabel(entry, onlySelectKeys)} + + ); +}; + +const toChips = ( + entries: TagSelectorEntry[], + onlySelectKeys: boolean, + onClick: (entry: TagSelectorEntry, edit: boolean) => void +) => { + return entries.map((entry) => ( + onClick(entry, !e.ctrlKey)} + /> + )); +}; diff --git a/ui/src/tag/suggest.ts b/ui/src/tag/suggest.ts new file mode 100644 index 0000000..040fcbc --- /dev/null +++ b/ui/src/tag/suggest.ts @@ -0,0 +1,90 @@ +import {Tags} from '../gql/__generated__/Tags'; +import * as gqlTags from '../gql/tags'; +import {useQuery} from '@apollo/react-hooks'; +import {SuggestTagValue, SuggestTagValueVariables} from '../gql/__generated__/SuggestTagValue'; +import {TagSelectorEntry, specialTag} from './tagSelectorEntry'; +import {QueryResult} from 'react-apollo'; + +export const useSuggest = ( + tagResult: QueryResult, + inputValue: string, + usedTags: TagSelectorEntry[], + skipValue = false, + allowDuplicateKeys = false, + createTags = true +): TagSelectorEntry[] => { + const [tagKeySomeCase, tagValue] = inputValue.split(':'); + const tagKey = tagKeySomeCase.toLowerCase(); + + const exactMatch = ((tagResult.data && tagResult.data.tags) || []).find((tag) => tag.key === tagKey); + + const valueResult = useQuery(gqlTags.SuggestTagValue, { + variables: {tag: tagKey, query: tagValue}, + skip: exactMatch === undefined || skipValue, + fetchPolicy: 'no-cache', + }); + + const usedKeys = usedTags.map((t) => t.tag.key); + + if (exactMatch && tagValue !== undefined && !skipValue && (allowDuplicateKeys || usedKeys.indexOf(exactMatch.key) === -1)) { + const usedValues = usedTags.filter((t) => t.tag.key === tagKey).map((t) => t.value); + return suggestTagValue(exactMatch, tagValue, valueResult, usedValues, createTags); + } else { + return suggestTag(exactMatch, tagResult, tagKey, usedKeys, allowDuplicateKeys, createTags); + } +}; + +const suggestTag = ( + exactMatch: TagSelectorEntry['tag'] | undefined, + tagResult: QueryResult, + tagKey: string, + usedTags: string[], + allowDuplicateKeys: boolean, + createTags: boolean +) => { + if (!tagResult.data || tagResult.data.tags === null) { + return []; + } + + let availableTags = (tagResult.data.tags || []) + .filter((tag) => allowDuplicateKeys || usedTags.indexOf(tag.key) === -1) + .filter((tag) => tag.key.indexOf(tagKey) === 0); + + if (tagKey && !exactMatch && createTags) { + availableTags = [specialTag(tagKey, 'new'), ...availableTags]; + } + + if (usedTags.indexOf(tagKey) !== -1 && !allowDuplicateKeys) { + availableTags = [specialTag(tagKey, 'used'), ...availableTags]; + } + + return availableTags + .sort((a, b) => b.usages - a.usages) + .slice(0, 5) + .map((tag) => ({tag, value: ''})); +}; + +const suggestTagValue = ( + exactMatch: TagSelectorEntry['tag'], + tagValue: string, + valueResult: QueryResult, + usedValues: string[], + includeInputValueOnNoMatch: boolean +): TagSelectorEntry[] => { + let someValues = (valueResult.data && valueResult.data.values) || []; + + if (includeInputValueOnNoMatch && someValues.indexOf(tagValue) === -1) { + someValues = [tagValue, ...someValues]; + } + + if (someValues.length === 0 && !includeInputValueOnNoMatch) { + return [{tag: specialTag(exactMatch.key, 'no_values'), value: tagValue}]; + } + + someValues = someValues.filter((val) => usedValues.indexOf(val) === -1); + if (someValues.length === 0 && !includeInputValueOnNoMatch) { + return [{tag: specialTag(exactMatch.key, 'all_values_used'), value: ''}]; + } + + return someValues.map((val) => ({tag: exactMatch, value: val})); +}; diff --git a/ui/src/tag/tagSelectorEntry.ts b/ui/src/tag/tagSelectorEntry.ts new file mode 100644 index 0000000..b7ceb92 --- /dev/null +++ b/ui/src/tag/tagSelectorEntry.ts @@ -0,0 +1,141 @@ +import {Tags, Tags_tags} from '../gql/__generated__/Tags'; +import {QueryResult} from 'react-apollo'; + +export interface TagInputError { + error: string; + value: string; +} + +type SpecialTagState = 'used' | 'new' | 'no_values' | 'all_values_used'; +export interface SpecialTag { + create?: boolean; + alreadyUsed?: boolean; + noValues?: boolean; + allValuesUsed?: boolean; +} + +export interface TagSelectorEntry { + tag: Omit & SpecialTag; + value: string; +} + +export interface InputTag { + key: string; + value: string; +} + +export const toInputTags = (entries: TagSelectorEntry[]): InputTag[] => { + return entries.map((entry) => ({key: entry.tag.key, value: entry.value})); +}; + +export const toTagSelectorEntry = (tags: Array, entries: InputTag[]): TagSelectorEntry[] => { + return entries.map( + (timerTag): TagSelectorEntry => { + const definition = tags.find((tag) => tag.key === timerTag.key) || specialTag(timerTag.key, 'new'); + return { + tag: definition, + value: timerTag.value, + }; + } + ); +}; + +export const specialTag = (name: string, state: SpecialTagState): TagSelectorEntry['tag'] & {usages: 0} => { + return { + key: name, + __typename: 'TagDefinition', + color: 'gray', + create: state === 'new', + alreadyUsed: state === 'used', + noValues: state === 'no_values', + allValuesUsed: state === 'all_values_used', + usages: 0, + }; +}; + +const tryAdd = (tagsResult: QueryResult, entry: string, onlyKeys: boolean): TagSelectorEntry | TagInputError => { + const [keySomeCase, value, ...other] = entry.split(':'); + const key = keySomeCase.toLowerCase(); + + if (other.length || !tagsResult.data || !tagsResult.data.tags) { + return {error: `${entry} has too many colons`, value: entry}; + } + + const foundTag = tagsResult.data.tags.find((tag) => tag.key === key); + + if (foundTag === undefined) { + return {error: `'${key}' does not exist`, value: entry}; + } + + if (onlyKeys && value) { + return {error: `'${key}' has a value, but this field doesn't allow them`, value: entry}; + } + + if (!onlyKeys && !value) { + return {error: `'${key}' requires a value`, value: entry}; + } + + return {tag: foundTag, value: value || ''}; +}; +interface EntriesAndErrors { + errors: TagInputError[]; + entries: TagSelectorEntry[]; + usedTags: string[]; +} + +const groupAndCheckExistence = (onlyKeys: boolean, allowDuplicateKeys: boolean) => { + return (a: EntriesAndErrors, entry: TagInputError | TagSelectorEntry) => { + if ('tag' in entry) { + if (allowDuplicateKeys || a.usedTags.indexOf(entry.tag.key) === -1) { + a.entries = [...a.entries, entry]; + a.usedTags = [...a.usedTags, entry.tag.key]; + } else { + a.errors = [...a.errors, {value: itemLabel(entry, onlyKeys), error: `'${entry.tag.key}' is already defined`}]; + } + } else { + a.errors = [...a.errors, entry]; + } + return a; + }; +}; + +export const addValues = ( + newValue: string, + tagsResult: QueryResult, + selectedEntries: TagSelectorEntry[], + onlyKeys: boolean, + allowDuplicateKeys: boolean +): EntriesAndErrors => { + return newValue + .split(/\s+/) + .filter((entry) => entry) + .map((entry) => tryAdd(tagsResult, entry, onlyKeys)) + .reduce(groupAndCheckExistence(onlyKeys, allowDuplicateKeys), { + errors: [], + entries: [], + usedTags: selectedEntries.map((entry) => entry.tag.key), + }); +}; + +export const itemLabel = (tag: TagSelectorEntry, onlyShowKey = false) => { + if (tag.tag.create) { + return `Create tag '${tag.tag.key}'`; + } + if (tag.tag.alreadyUsed) { + return `Tag '${tag.tag.key}' is already defined`; + } + if (tag.tag.noValues) { + return `Unkown value '${tag.value}' of tag '${tag.tag.key}'`; + } + if (tag.tag.allValuesUsed) { + return `All values of tag '${tag.tag.key}' are used`; + } + if (onlyShowKey) { + return tag.tag.key; + } + return label(tag); +}; + +export const label = (tag: TagSelectorEntry) => { + return `${tag.tag.key}:${tag.value}`; +}; diff --git a/ui/src/timespan/ActiveTrackers.tsx b/ui/src/timespan/ActiveTrackers.tsx new file mode 100644 index 0000000..175b65e --- /dev/null +++ b/ui/src/timespan/ActiveTrackers.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import {useQuery} from '@apollo/react-hooks'; +import * as gqlTimeSpan from '../gql/timeSpan'; +import * as gqlTag from '../gql/tags'; +import {Trackers} from '../gql/__generated__/Trackers'; +import {Tags} from '../gql/__generated__/Tags'; +import {TimeSpan} from './TimeSpan'; +import {toTimeSpanProps} from './timespanutils'; +import {Typography} from '@material-ui/core'; + +export const ActiveTrackers = () => { + const trackersResult = useQuery(gqlTimeSpan.Trackers, {fetchPolicy: 'cache-and-network'}); + const tagsResult = useQuery(gqlTag.Tags, {fetchPolicy: 'cache-and-network'}); + const values = React.useMemo(() => { + if ( + trackersResult.error || + trackersResult.loading || + !trackersResult.data || + trackersResult.data.timers === null || + tagsResult.error || + tagsResult.loading || + !tagsResult.data || + tagsResult.data.tags === null + ) { + return []; + } + return toTimeSpanProps(trackersResult.data.timers, tagsResult.data.tags); + }, [tagsResult, trackersResult]); + + if (!values.length) { + return null; + } + + return ( + <> + + Active Timers + + {values.map((value) => { + return ; + })} + + ); +}; diff --git a/ui/src/timespan/DailyPage.tsx b/ui/src/timespan/DailyPage.tsx new file mode 100644 index 0000000..4d5955a --- /dev/null +++ b/ui/src/timespan/DailyPage.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import {Tracker} from './Tracker'; +import {ActiveTrackers} from './ActiveTrackers'; +import {DoneTrackers} from './DoneTrackers'; +import {TagSelectorEntry} from '../tag/tagSelectorEntry'; +import {RefreshTimeSpans} from './RefreshTimespans'; + +export const DailyPage = () => { + const [selectedEntries, setSelectedEntries] = React.useState([]); + return ( +
+ + + setSelectedEntries(selectedEntries.concat(entries)) : undefined + } + /> + +
+ ); +}; diff --git a/ui/src/timespan/DoneTrackers.tsx b/ui/src/timespan/DoneTrackers.tsx new file mode 100644 index 0000000..26c0c26 --- /dev/null +++ b/ui/src/timespan/DoneTrackers.tsx @@ -0,0 +1,149 @@ +import * as React from 'react'; +import {useQuery} from '@apollo/react-hooks'; +import * as gqlTimeSpan from '../gql/timeSpan'; +import * as gqlTag from '../gql/tags'; +import {TimeSpan, TimeSpanProps} from './TimeSpan'; +import {Tags} from '../gql/__generated__/Tags'; +import useInterval from '@rooks/use-interval'; +import moment from 'moment'; +import {TimeSpans, TimeSpansVariables} from '../gql/__generated__/TimeSpans'; +import {Typography} from '@material-ui/core'; +import {GroupedTimeSpanProps, toGroupedTimeSpanProps} from './timespanutils'; +import {TagSelectorEntry} from '../tag/tagSelectorEntry'; +import ReactInfinite from 'react-infinite'; +import {isSameDate} from '../utils/time'; + +interface DoneTrackersProps { + addTagsToTracker?: (entries: TagSelectorEntry[]) => void; +} + +export const DoneTrackers: React.FC = ({addTagsToTracker}) => { + const trackersResult = useQuery(gqlTimeSpan.TimeSpans, { + variables: {cursor: {pageSize: 30}}, + }); + const loading = React.useRef(false); + const tagsResult = useQuery(gqlTag.Tags); + const [infiniteLoading, setInfiniteLoading] = React.useState(false); + const [currentDate, setCurrentDate] = React.useState(moment()); + const [heights, setHeights] = React.useState>({}); + useInterval( + () => { + if (!isSameDate(currentDate, moment())) { + setCurrentDate(moment()); + } + }, + 1000, + true + ); + + const fetchMore = () => { + if (!trackersResult || !trackersResult.data || trackersResult.loading || loading.current) { + return; + } + loading.current = true; + const {offset, pageSize, startId} = trackersResult.data.timeSpans.cursor; + trackersResult + .fetchMore({ + variables: { + cursor: { + startId, + offset, + pageSize, + }, + }, + updateQuery: (prev, {fetchMoreResult}): TimeSpans => { + if (!fetchMoreResult) { + return prev; + } + + return { + timeSpans: { + __typename: 'PagedTimeSpans', + timeSpans: [...prev.timeSpans.timeSpans, ...fetchMoreResult.timeSpans.timeSpans], + cursor: fetchMoreResult.timeSpans.cursor, + }, + }; + }, + }) + .then(() => { + loading.current = false; + return setInfiniteLoading(false); + }) + .catch(() => { + loading.current = false; + return setInfiniteLoading(false); + }); + }; + + const values: GroupedTimeSpanProps = React.useMemo(() => { + if ( + trackersResult.error || + trackersResult.loading || + !trackersResult.data || + trackersResult.data.timeSpans === null || + tagsResult.error || + tagsResult.loading || + !tagsResult.data || + tagsResult.data.tags === null + ) { + return []; + } + return toGroupedTimeSpanProps(trackersResult.data.timeSpans.timeSpans, tagsResult.data.tags, currentDate); + }, [trackersResult, tagsResult, currentDate]); + + return ( +
+ + .. loading time spans .. + + } + elementHeight={values.map((m) => heights[m.key] || 500)}> + {values.map(({key, timeSpans}) => { + return ( + + ); + })} + +
+ ); +}; + +const DatedTimeSpans: React.FC<{ + name: string; + setHeight: (cb: (height: Record) => Record) => void; + height: number; + timeSpans: TimeSpanProps[]; +} & DoneTrackersProps> = ({name, timeSpans, addTagsToTracker, setHeight, height}) => { + const ref = React.useRef(); + React.useEffect(() => { + const currentHeight = ref.current && ref.current.getBoundingClientRect().height; + if (currentHeight != null && currentHeight !== height) { + setHeight((old) => ({...old, [name]: currentHeight})); + } + }, [ref, name, setHeight, height]); + return ( +
(ref.current = r)}> + + {name} + + {timeSpans.map((timeSpanProps) => ( + + ))} +
+ ); +}; diff --git a/ui/src/timespan/RefreshTimespans.tsx b/ui/src/timespan/RefreshTimespans.tsx new file mode 100644 index 0000000..1d336b1 --- /dev/null +++ b/ui/src/timespan/RefreshTimespans.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import {useQuery} from '@apollo/react-hooks'; +import {TimeSpans, TimeSpansVariables} from '../gql/__generated__/TimeSpans'; +import * as gqlTimeSpan from '../gql/timeSpan'; +import {useSnackbar} from 'notistack'; +import {isSameDate} from '../utils/time'; +import moment from 'moment'; +import {Fab, Zoom} from '@material-ui/core'; +import RefreshIcon from '@material-ui/icons/Refresh'; + +export const RefreshTimeSpans: React.FC = () => { + const {refetch, data} = useQuery(gqlTimeSpan.TimeSpans, { + variables: {cursor: {pageSize: 30}}, + }); + const {enqueueSnackbar, closeSnackbar} = useSnackbar(); + const hasMovedEntries = + data && + data.timeSpans && + data.timeSpans.timeSpans.some(({start, oldStart}) => !isSameDate(moment(start), oldStart ? moment(oldStart) : undefined)); + React.useEffect(() => { + if (hasMovedEntries) { + const id = enqueueSnackbar('Some messages where moved, use the refresh button to reorder the timespans', { + variant: 'info', + persist: true, + preventDuplicate: true, + }); + return () => { + if (id) { + closeSnackbar(id); + } + }; + } + return () => {}; + }, [hasMovedEntries, enqueueSnackbar, closeSnackbar]); + + if (!hasMovedEntries) { + return <>; + } + + return ( + + { + refetch({cursor: {pageSize: 30}}).then(() => { + enqueueSnackbar('Refreshed messages', {variant: 'success'}); + }); + }} + style={{position: 'fixed', bottom: '30px', right: '30px', zIndex: 100000}} + color="primary"> + + + + ); +}; diff --git a/ui/src/timespan/TimeSpan.tsx b/ui/src/timespan/TimeSpan.tsx new file mode 100644 index 0000000..b70ba9a --- /dev/null +++ b/ui/src/timespan/TimeSpan.tsx @@ -0,0 +1,363 @@ +import * as React from 'react'; +import {TagSelectorEntry, toInputTags} from '../tag/tagSelectorEntry'; +import {TagSelector} from '../tag/TagSelector'; +import moment from 'moment'; +import Paper from '@material-ui/core/Paper'; +import {DateTimeSelector} from '../common/DateTimeSelector'; +import {Button, TextField, Typography, makeStyles} from '@material-ui/core'; +import {inUserTz} from './timeutils'; +import {useMutation} from '@apollo/react-hooks'; +import {StopTimer, StopTimerVariables} from '../gql/__generated__/StopTimer'; +import * as gqlTimeSpan from '../gql/timeSpan'; +import {UpdateTimeSpan, UpdateTimeSpanVariables} from '../gql/__generated__/UpdateTimeSpan'; +import IconButton from '@material-ui/core/IconButton'; +import {MoreVert} from '@material-ui/icons'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import {RemoveTimeSpan, RemoveTimeSpanVariables} from '../gql/__generated__/RemoveTimeSpan'; +import {useStateAndDelegateWithDelayOnChange} from '../utils/hooks'; +import {TimeSpans} from '../gql/__generated__/TimeSpans'; +import {isSameDate} from '../utils/time'; +import {Trackers} from '../gql/__generated__/Trackers'; +import {addTimeSpanToCache, removeFromTrackersCache} from '../gql/utils'; +import {StartTimer, StartTimerVariables} from '../gql/__generated__/StartTimer'; +import {RelativeTime, RelativeToNow} from '../common/RelativeTime'; + +interface Range { + from: moment.Moment; + to?: moment.Moment; +} + +export interface TimeSpanProps { + id: number; + range: Range & {oldFrom?: moment.Moment}; + initialTags: TagSelectorEntry[]; + note: string; + dateSelectorOpen?: React.Dispatch>; + rangeChange?: (r: Range) => void; + deleted?: () => void; + stopped?: () => void; + continued?: () => void; + addTagsToTracker?: (tags: TagSelectorEntry[]) => void; + elevation?: number; +} + +const useStyles = makeStyles(() => ({ + innerTimespan: { + position: 'relative', + width: '100%', + display: 'flex', + alignItems: 'center', + textAlign: 'center', + '@media (max-width: 750px)': { + flexDirection: 'column', + }, + }, + tagInput: { + width: '100%', + flex: '1', + marginRight: 10, + '@media (max-width: 750px)': { + display: 'flex', + width: 'calc(100% - 48px)', + marginRight: '48px', + }, + }, + timeSelection: { + display: 'inline-flex', + '@media (max-width: 750px)': { + justifyContent: 'space-evenly', + width: '100%', + flexWrap: 'wrap', + }, + }, + showMoreButton: { + display: 'flex', + alignItems: 'center', + '@media (max-width: 750px)': { + position: 'absolute', + top: '0', + right: '0', + }, + }, +})); + +export const TimeSpan: React.FC = React.memo( + ({ + range: {from, to, oldFrom}, + id, + initialTags, + note: initialNote, + dateSelectorOpen = () => {}, + rangeChange = () => {}, + deleted = () => {}, + stopped = () => {}, + continued = () => {}, + elevation = 1, + addTagsToTracker, + }) => { + const styles = useStyles(); + const [showNotes, toggleShowingNotes] = React.useState(initialNote !== ''); + const note = React.useRef<{value: string; handle?: number}>({value: initialNote}); + + const [selectedEntries, setSelectedEntries] = React.useState(initialTags); + const [openMenu, setOpenMenu] = useStateAndDelegateWithDelayOnChange(null, (o) => + dateSelectorOpen(!!o) + ); + const [stopTimer] = useMutation(gqlTimeSpan.StopTimer, { + update: (cache, {data}) => { + if (!data || !data.stopTimeSpan) { + return; + } + removeFromTrackersCache(cache, data); + addTimeSpanToCache(cache, data.stopTimeSpan); + }, + }); + const [startTimer] = useMutation(gqlTimeSpan.StartTimer, { + refetchQueries: [{query: gqlTimeSpan.Trackers}], + }); + const [updateTimeSpan] = useMutation(gqlTimeSpan.UpdateTimeSpan); + const noteAwareUpdateTimeSpan = ({variables}: {variables: Omit}) => { + clearTimeout(note.current.handle); + return updateTimeSpan({variables: {...variables, note: note.current.value}}); + }; + const [removeTimeSpan] = useMutation(gqlTimeSpan.RemoveTimeSpan, { + update: (cache, {data}) => { + let oldData: TimeSpans | null = null; + try { + oldData = cache.readQuery({query: gqlTimeSpan.TimeSpans}); + } catch (e) {} + + const oldTrackers = cache.readQuery({query: gqlTimeSpan.Trackers}); + if (!data || !data.removeTimeSpan) { + return; + } + const removedId = data.removeTimeSpan.id; + if (oldTrackers) { + cache.writeQuery({ + query: gqlTimeSpan.Trackers, + data: { + timers: (oldTrackers.timers || []).filter((tracker) => tracker.id !== removedId), + }, + }); + } + if (oldData) { + cache.writeQuery({ + query: gqlTimeSpan.TimeSpans, + data: { + timeSpans: { + __typename: 'PagedTimeSpans', + timeSpans: oldData.timeSpans.timeSpans.filter((ts) => ts.id !== removedId), + cursor: oldData.timeSpans.cursor, + }, + }, + }); + } + }, + }); + + const updateNote = (newValue: string) => { + window.clearTimeout(note.current.handle); + const handle = window.setTimeout( + () => + updateTimeSpan({ + variables: { + oldStart: oldFrom, + id, + start: inUserTz(from).format(), + end: to && inUserTz(to).format(), + tags: toInputTags(selectedEntries), + note: newValue, + }, + }), + 200 + ); + note.current = {handle, value: newValue}; + }; + + const wasMoved = !isSameDate(from, oldFrom); + const showDate = to !== undefined && (!isSameDate(from, to) || wasMoved); + return ( + +
+
+ { + setSelectedEntries(entries); + noteAwareUpdateTimeSpan({ + variables: { + oldStart: oldFrom, + id, + start: inUserTz(from).format(), + end: to && inUserTz(to).format(), + tags: toInputTags(entries), + }, + }); + }} + /> +
+ +
+
+ { + if (!newFrom.isValid()) { + return; + } + newFrom.set({second: 0}); + if (to && moment(newFrom).isAfter(to)) { + const newTo = moment(newFrom).add(15, 'minute'); + noteAwareUpdateTimeSpan({ + variables: { + oldStart: oldFrom, + id, + start: inUserTz(newFrom).format(), + end: inUserTz(newTo).format(), + tags: toInputTags(selectedEntries), + }, + }).then(() => rangeChange({from: newFrom, to: newTo})); + } else { + noteAwareUpdateTimeSpan({ + variables: { + id, + oldStart: oldFrom, + start: inUserTz(newFrom).format(), + end: to && inUserTz(to).format(), + tags: toInputTags(selectedEntries), + }, + }).then(() => rangeChange({from: newFrom, to})); + } + }} + showDate={showDate} + label="start" + /> + {to !== undefined ? ( + { + if (!newTo.isValid()) { + return; + } + newTo.set({second: 0}); + if (moment(newTo).isBefore(from)) { + const newFrom = moment(newTo).subtract(15, 'minute'); + noteAwareUpdateTimeSpan({ + variables: { + id, + oldStart: oldFrom, + start: inUserTz(newFrom).format(), + end: inUserTz(newTo).format(), + tags: toInputTags(selectedEntries), + }, + }).then(() => rangeChange({from: newFrom, to: newTo})); + } else { + noteAwareUpdateTimeSpan({ + variables: { + id, + oldStart: oldFrom, + start: inUserTz(from).format(), + end: inUserTz(newTo).format(), + tags: toInputTags(selectedEntries), + }, + }).then(() => rangeChange({from, to: newTo})); + } + }} + showDate={showDate} + label="end" + /> + ) : ( + + )} +
+ +
+ + {to ? : } + +
+
+ + ) => setOpenMenu(e.currentTarget)}> + + + + setOpenMenu(null)}> + {to ? ( + { + setOpenMenu(null); + startTimer({ + variables: { + start: inUserTz(moment()).format(), + tags: toInputTags(selectedEntries), + note: note.current.value, + }, + }).then(() => continued()); + }}> + Continue + + ) : null} + {addTagsToTracker ? ( + { + setOpenMenu(null); + addTagsToTracker(selectedEntries); + }}> + Copy tags + + ) : null} + { + setOpenMenu(null); + toggleShowingNotes(!showNotes); + }}> + Show Notes + + { + setOpenMenu(null); + removeTimeSpan({variables: {id}}).then(() => deleted()); + }}> + Delete + + +
+ {showNotes ? ( +
+ updateNote(e.target.value)} + /> +
+ ) : null} +
+ ); + } +); diff --git a/ui/src/timespan/Tracker.tsx b/ui/src/timespan/Tracker.tsx new file mode 100644 index 0000000..da53879 --- /dev/null +++ b/ui/src/timespan/Tracker.tsx @@ -0,0 +1,157 @@ +import * as React from 'react'; +import {TagSelectorEntry} from '../tag/tagSelectorEntry'; +import {TagSelector} from '../tag/TagSelector'; +import moment from 'moment-timezone'; +import {Button} from '@material-ui/core'; +import {MoreVert} from '@material-ui/icons'; +import IconButton from '@material-ui/core/IconButton'; +import Paper from '@material-ui/core/Paper'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import {DateTimeSelector} from '../common/DateTimeSelector'; +import {useMutation} from '@apollo/react-hooks'; +import * as gqlTimeSpan from '../gql/timeSpan'; +import {StartTimer, StartTimerVariables} from '../gql/__generated__/StartTimer'; +import {InputTimeSpanTag} from '../gql/__generated__/globalTypes'; +import {AddTimeSpan, AddTimeSpanVariables} from '../gql/__generated__/AddTimeSpan'; +import {useSnackbar} from 'notistack'; +import {inUserTz} from './timeutils'; +import {addTimeSpanToCache} from '../gql/utils'; + +enum Type { + Tracker, + Manual, +} + +export const calcShowDate = (from: moment.Moment, to: moment.Moment) => { + const fromString = from.format('YYYYMMDD'); + return fromString !== to.format('YYYYMMDD') || moment().format('YYYYMMDD') !== fromString; +}; + +interface TrackerProps { + onSelectedEntriesChanged: (entries: TagSelectorEntry[]) => void; + selectedEntries: TagSelectorEntry[]; +} + +export const Tracker: React.FC = ({selectedEntries, onSelectedEntriesChanged: setSelectedEntries}) => { + const [openMenu, setOpenMenu] = React.useState(null); + const [type, setType] = React.useState(Type.Tracker); + const [from, setFrom] = React.useState(moment().subtract(15, 'minute')); + const [to, setTo] = React.useState(moment()); + const [showDate, setShowDate] = React.useState(false); + const [startTimer] = useMutation(gqlTimeSpan.StartTimer, { + refetchQueries: [{query: gqlTimeSpan.Trackers}], + }); + const [addTimeSpan] = useMutation(gqlTimeSpan.AddTimeSpan, { + update: (cache, {data}) => { + if (!data || !data.createTimeSpan) { + return; + } + addTimeSpanToCache(cache, data.createTimeSpan); + }, + }); + const {enqueueSnackbar} = useSnackbar(); + + React.useEffect(() => { + const handle = window.setInterval(() => { + setShowDate(calcShowDate(from, to)); + }, 10000); + return () => clearInterval(handle); + }, [showDate, from, to]); + + const submit = () => { + const tags = selectedEntries.map( + (entry: TagSelectorEntry): InputTimeSpanTag => ({key: entry.tag.key, value: entry.value}) + ); + if (type === Type.Tracker) { + startTimer({variables: {start: inUserTz(moment()).format(), tags, note: ''}}).then(() => { + setSelectedEntries([]); + enqueueSnackbar('tracker started', {variant: 'success'}); + }); + } else { + addTimeSpan({variables: {start: inUserTz(from).format(), end: inUserTz(to).format(), tags, note: ''}}).then(() => { + setSelectedEntries([]); + enqueueSnackbar('time span added', {variant: 'success'}); + }); + } + }; + + return ( + setOpenMenu(null)}> + +
+ +
+ {type === Type.Manual ? ( +
+ { + if (!newFrom.isValid()) { + return; + } + setFrom(newFrom); + if (moment(newFrom).isAfter(to)) { + const newTo = moment(newFrom).add(15, 'minute'); + setTo(newTo); + setShowDate(calcShowDate(newFrom, newTo)); + } else { + setShowDate(calcShowDate(newFrom, to)); + } + }} + showDate={showDate} + label="start" + /> + { + if (!newTo.isValid()) { + return; + } + setTo(newTo); + if (moment(newTo).isBefore(from)) { + const newFrom = moment(newTo).subtract(15, 'minute'); + setFrom(newFrom); + setShowDate(calcShowDate(newFrom, newTo)); + } else { + setShowDate(calcShowDate(from, newTo)); + } + }} + showDate={showDate} + label="end" + /> +
+ ) : null} + + ) => setOpenMenu(e.currentTarget)}> + + + + { + setOpenMenu(null); + setType(Type.Tracker); + }}> + Tracker + + { + setOpenMenu(null); + setType(Type.Manual); + }}> + Manual + + +
+
+ ); +}; diff --git a/ui/src/timespan/calendar/CalendarPage.tsx b/ui/src/timespan/calendar/CalendarPage.tsx new file mode 100644 index 0000000..0ebd143 --- /dev/null +++ b/ui/src/timespan/calendar/CalendarPage.tsx @@ -0,0 +1,380 @@ +import * as React from 'react'; +import {Paper, useTheme} from '@material-ui/core'; +import moment from 'moment'; +import {useApolloClient, useMutation, useQuery} from '@apollo/react-hooks'; +import {TimeSpans_timeSpans_timeSpans} from '../../gql/__generated__/TimeSpans'; +import * as gqlTimeSpan from '../../gql/timeSpan'; +import {Trackers} from '../../gql/__generated__/Trackers'; +import {Tags} from '../../gql/__generated__/Tags'; +import * as gqlTag from '../../gql/tags'; +import FullCalendar from '@fullcalendar/react'; +import {calculateColor, ColorMode} from '../colorutils'; +import '@fullcalendar/core/main.css'; +import '@fullcalendar/daygrid/main.css'; +import '@fullcalendar/timegrid/main.css'; +import dayGridPlugin from '@fullcalendar/daygrid'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import momentPlugin from '@fullcalendar/moment'; +import interactionPlugin from '@fullcalendar/interaction'; +import {OptionsInput} from '@fullcalendar/core'; +import {UpdateTimeSpan, UpdateTimeSpanVariables} from '../../gql/__generated__/UpdateTimeSpan'; +import Popper from '@material-ui/core/Popper'; +import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import {TimeSpan} from '../TimeSpan'; +import {toTagSelectorEntry} from '../../tag/tagSelectorEntry'; +import {AddTimeSpan, AddTimeSpanVariables} from '../../gql/__generated__/AddTimeSpan'; +import {FullCalendarStyling} from './FullCalendarStyling'; +import useInterval from '@rooks/use-interval'; +import {EventApi} from '@fullcalendar/core/api/EventApi'; +import {StopTimer, StopTimerVariables} from '../../gql/__generated__/StopTimer'; +import { + addTimeSpanInRangeToCache, + addTimeSpanToCache, + removeFromTimeSpanInRangeCache, + removeFromTrackersCache, +} from '../../gql/utils'; +import {StartTimer, StartTimerVariables} from '../../gql/__generated__/StartTimer'; +import {timeRunningCalendar} from '../timeutils'; +import {stripTypename} from '../../utils/strip'; +import {TimeSpansInRange, TimeSpansInRangeVariables} from '../../gql/__generated__/TimeSpansInRange'; +import {ExtendedEventSourceInput} from '@fullcalendar/core/structs/event-source'; + +const toMoment = (date: Date): moment.Moment => { + return moment(date).tz('utc'); +}; + +declare global { + interface Window { + // tslint:disable-next-line:no-any + __TRAGGO_CALENDAR: any; + } +} + +const StartTimerId = '-1'; + +export const CalendarPage: React.FC = () => { + const apollo = useApolloClient(); + const theme = useTheme(); + const timeSpansResult = useQuery(gqlTimeSpan.TimeSpansInRange, { + variables: { + start: moment() + .startOf('week') + .format(), + end: moment() + .endOf('week') + .format(), + }, + fetchPolicy: 'cache-and-network', + }); + const trackersResult = useQuery(gqlTimeSpan.Trackers, {fetchPolicy: 'cache-and-network'}); + const tagsResult = useQuery(gqlTag.Tags); + const [startTimer] = useMutation(gqlTimeSpan.StartTimer, { + refetchQueries: [{query: gqlTimeSpan.Trackers}], + }); + const [updateTimeSpanMutation] = useMutation(gqlTimeSpan.UpdateTimeSpan); + const [currentDate, setCurrentDate] = React.useState(moment()); + const [stopTimer] = useMutation(gqlTimeSpan.StopTimer, { + update: (cache, {data}) => { + if (!data || !data.stopTimeSpan) { + return; + } + removeFromTrackersCache(cache, data); + addTimeSpanInRangeToCache(cache, data.stopTimeSpan, timeSpansResult.variables); + }, + }); + useInterval( + () => { + setCurrentDate(moment()); + }, + 60000, + true + ); + React.useEffect(() => { + window.__TRAGGO_CALENDAR = {}; + return () => (window.__TRAGGO_CALENDAR = undefined); + }); + const [ignore, setIgnore] = React.useState(false); + const [selected, setSelected] = React.useState<{selected: HTMLElement | null; data: TimeSpans_timeSpans_timeSpans | null}>({ + selected: null, + data: null, + }); + const [addTimeSpan] = useMutation(gqlTimeSpan.AddTimeSpan, { + update: (cache, {data}) => { + if (!data || !data.createTimeSpan) { + return; + } + addTimeSpanInRangeToCache(cache, data.createTimeSpan, timeSpansResult.variables); + addTimeSpanToCache(cache, data.createTimeSpan); + }, + }); + + const values: ExtendedEventSourceInput[] = (() => { + if ( + timeSpansResult.error || + timeSpansResult.loading || + !timeSpansResult.data || + timeSpansResult.data.timeSpans === null || + trackersResult.error || + trackersResult.loading || + !trackersResult.data || + trackersResult.data.timers === null || + tagsResult.error || + tagsResult.loading || + !tagsResult.data || + tagsResult.data.tags === null + ) { + return []; + } + return timeSpansResult.data.timeSpans.timeSpans + .concat(trackersResult.data.timers) + .sort((a, b) => a.start.toString().localeCompare(b.start.toString())) + .map((ts) => { + const colorKey = ts + .tags!.map((t) => t.key + ':' + t.value) + .sort((a, b) => a.localeCompare(b)) + .join(' '); + const color = calculateColor(colorKey, ColorMode.Bold, theme.palette.type); + const borderColor = calculateColor(colorKey, ColorMode.None, theme.palette.type); + return { + start: moment(ts.start).toDate(), + end: moment(ts.end || currentDate).toDate(), + hasEnd: !!ts.end, + editable: !!ts.end, + backgroundColor: color, + startEditable: true, + id: ts.id, + tags: ts.tags!.map(({value, key}) => ({key, value})), + title: ts.tags!.map((t) => t.key + ':' + t.value).join(' '), + extendedProps: {ts}, + textColor: theme.palette.getContrastText(color), + borderColor, + }; + }); + })(); + + const onDrop: OptionsInput['eventDrop'] = (data) => { + updateTimeSpanMutation({ + variables: { + oldStart: moment(data.oldEvent.start!).format(), + start: moment(data.event.start!).format(), + end: moment(data.event.end!).format(), + id: parseInt(data.event.id, 10), + tags: stripTypename(data.event.extendedProps.ts.tags), + note: data.event.extendedProps.ts.note, + }, + }); + }; + const onResize: OptionsInput['eventResize'] = (data) => { + updateTimeSpanMutation({ + variables: { + oldStart: moment(data.prevEvent.start!).format(), + start: moment(data.event.start!).format(), + end: moment(data.event.end!).format(), + id: parseInt(data.event.id, 10), + tags: stripTypename(data.event.extendedProps.ts.tags), + note: data.event.extendedProps.ts.note, + }, + }); + }; + const onSelect: OptionsInput['select'] = (data) => { + addTimeSpan({ + variables: { + start: moment(data.start).format(), + end: moment(data.end).format(), + tags: [], + note: '', + }, + }); + }; + const onClick: OptionsInput['eventClick'] = (data) => { + data.jsEvent.preventDefault(); + if (data.event.id === StartTimerId) { + startTimer({variables: {start: moment().format(), tags: [], note: ''}}).then(() => { + setCurrentDate(moment()); + }); + return; + } + + // tslint:disable-next-line:no-any + setSelected({data: data.event.extendedProps.ts, selected: data.jsEvent.target as any}); + }; + if (trackersResult.data && !(trackersResult.data.timers || []).length) { + const startTimerEvent: ExtendedEventSourceInput = { + start: currentDate.toDate(), + end: moment(currentDate) + .add(15, 'minute') + .toDate(), + className: '__start', + editable: false, + id: StartTimerId, + }; + values.push(startTimerEvent); + } + + return ( + + + { + const range = {start: moment(x.view.currentStart), end: moment(x.view.currentEnd)}; + if ( + !moment(timeSpansResult.variables.start).isSame(range.start) || + !moment(timeSpansResult.variables.end).isSame(range.end) + ) { + timeSpansResult.refetch(range); + } + }} + views={{ + timeGrid5Day: { + type: 'timeGrid', + duration: {days: 7}, + buttonText: '5 day', + hiddenDays: [0, 6], + }, + }} + editable={true} + events={values} + allDaySlot={false} + selectable={true} + selectMirror={true} + handleWindowResize={true} + height={'parent'} + selectMinDistance={20} + now={currentDate.toDate()} + defaultTimedEventDuration={{minute: 15}} + eventRender={(e) => { + const content = e.el.getElementsByClassName('fc-content').item(0); + if (content) { + content.innerHTML = getElementContent(e.event, () => { + stopTimer({ + variables: {id: e.event.extendedProps.ts.id, end: moment().format()}, + }); + }); + } + + e.el.setAttribute('data-has-end', '' + (!e.event.extendedProps.ts || !!e.event.extendedProps.ts.end)); + }} + slotLabelInterval={{minute: 60}} + slotDuration={{minute: 15}} + scrollTime={{hour: 6, minute: 30}} + select={onSelect} + firstDay={moment.localeData().firstDayOfWeek()} + eventResize={onResize} + eventClick={onClick} + eventDrop={onDrop} + slotLabelFormat={(s) => toMoment(s.start.marker).format('LT')} + columnHeaderFormat={(s) => toMoment(s.start.marker).format('DD ddd')} + nowIndicator={true} + plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin, momentPlugin]} + header={{ + center: 'title', + left: 'prev,next today', + right: 'timeGridWeek,timeGrid5Day,timeGridDay', + }} + /> + + {!!selected.selected && ( + + { + if (ignore) { + return; + } + setSelected({selected: null, data: null}); + }}> +
+ { + setSelected({ + ...selected, + data: {...selected.data!, start: range.from.format(), end: range.to && range.to.format()}, + }); + }} + deleted={() => { + removeFromTimeSpanInRangeCache(apollo.cache, selected.data!.id, timeSpansResult.variables); + setSelected({selected: null, data: null}); + }} + continued={() => setCurrentDate(moment())} + range={{ + from: moment(selected.data!.start), + to: selected.data!.end ? moment(selected.data!.end) : undefined, + }} + initialTags={toTagSelectorEntry( + tagsResult.data!.tags!, + selected.data!.tags!.map((tag) => ({key: tag.key, value: tag.value})) + )} + note={selected.data!.note} + dateSelectorOpen={setIgnore} + stopped={() => { + setSelected({ + ...selected, + data: {...selected.data!, end: currentDate.toDate()}, + }); + }} + /> +
+
+
+ )} +
+ ); +}; + +const getElementContent = (event: EventApi, stop: () => void): string => { + if (!event.start || !event.end) { + return ''; + } + + if (event.id === StartTimerId) { + return 'START'; + } + + const start = moment(event.start); + const end = moment(event.end); + const diff = end.diff(start, 'minute'); + + const lines = Math.floor(diff / 15); + const hasEnd = !event.extendedProps.ts || event.extendedProps.ts.end; + + let stopButton = ''; + if (!hasEnd) { + const id = event.extendedProps.ts.id; + if (!window.__TRAGGO_CALENDAR) { + window.__TRAGGO_CALENDAR = {}; + } + window.__TRAGGO_CALENDAR[id] = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + stop(); + return false; + }; + stopButton = ``; + } + const clamp = (amount: number) => + `${event.title}`; + + const running = hasEnd ? `${timeRunningCalendar(start, end)}` : ''; + const date = `${start.format('LT')} - ${hasEnd ? end.format('LT') : 'now'} ${running}`; + if (lines < 2) { + return event.title + ? `${event.title}${stopButton}` + : `${date}${stopButton}`; + } + if (lines === 2) { + if (hasEnd) { + return `${date}${event.title}${stopButton}`; + } else { + return `${clamp(2)}${stopButton}`; + } + } + + return `${date}
${clamp(lines - 1)}${stopButton}`; +}; diff --git a/ui/src/timespan/calendar/FullCalendarStyling.tsx b/ui/src/timespan/calendar/FullCalendarStyling.tsx new file mode 100644 index 0000000..f07fba2 --- /dev/null +++ b/ui/src/timespan/calendar/FullCalendarStyling.tsx @@ -0,0 +1,236 @@ +import * as React from 'react'; +import {makeStyles} from '@material-ui/core'; + +const useStyle = makeStyles((theme) => { + return { + root: { + height: '100%', + width: '100%', + // '& .fc-time-grid-event': { + // opacity: 0, + // border: 'none', + // }, + '& .fc-head': { + fontFamily: 'monospace', + }, + '& .fc-toolbar': { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }, + '& .fc-left, .fc-center, .fc-right': { + display: 'inline-block', + verticalAlign: 'middle', + textAlign: 'center', + // width: '33.33%', + boxSizing: 'border-box', + // padding: '0 10px', + }, + '& .fc-center h2': { + margin: 0, + fontSize: '1.5rem', + }, + '@media (max-width: 600px)': { + '& .fc-toolbar': { + display: 'inherit', + justifyContent: 'space-between', + alignItems: 'center', + }, + '& .fc-left, .fc-center, .fc-right': { + display: 'block', + width: '100%', + }, + '& .fc-left, .fc-right': { + textAlign: 'center', + }, + }, + '& .fc-time-grid-event.fc-v-event.fc-event': { + borderRadius: 1, + border: 'none', + padding: 5, + opacity: 0.65, + left: '5% !important', + right: '5% !important', + }, + '& .fc .fc-time-grid-event': { + minHeight: 1, + }, + '& .fc-event': { + fontSize: 'inherit !important', + borderRadius: 1, + }, + '& .fc td, .fc th': { + borderStyle: 'solid !important', + borderWidth: '0px !important', + padding: '0 !important', + verticalAlign: 'top !important', + }, + '& .fc tr:nth-child(4n)': { + borderStyle: 'solid !important', + borderColor: theme.palette.type === 'dark' ? 'rgba(255,255,255,.2)' : 'rgba(0,0,0,.2)', + borderWidth: '0 0 2px 0 !important', + padding: '0 !important', + verticalAlign: 'top !important', + }, + '& .fc tbody tr:first-child': { + borderStyle: 'solid !important', + borderColor: theme.palette.type === 'dark' ? 'rgba(255,255,255,.2)' : 'rgba(0,0,0,.2)', + borderWidth: '2px 0 0 0 !important', + padding: '0 !important', + verticalAlign: 'top !important', + }, + '& .fc td': { + borderStyle: 'solid !important', + borderColor: theme.palette.type === 'dark' ? 'rgba(255,255,255,.1)' : 'rgba(0,0,0,.1)', + borderWidth: '0 1px 0 0 !important', + padding: '0 !important', + verticalAlign: 'top !important', + }, + '& .fc tr:nth-child(4n+2)': { + borderStyle: 'solid !important', + borderColor: theme.palette.type === 'dark' ? 'rgba(255,255,255,.1)' : 'rgba(0,0,0,.1)', + borderWidth: '0 0 1px 0 !important', + padding: '0 !important', + verticalAlign: 'top !important', + }, + '& .fc-event .fc-bg': { + zIndex: '1 !important', + background: 'inherit !important', + opacity: '.25 !important', + }, + '& .fc-time-grid-event .fc-time': { + fontWeight: 'normal !important', + }, + '& .fc-ltr .fc-h-event.fc-not-end, .fc-rtl .fc-h-event.fc-not-start': { + opacity: '.65 !important', + marginLeft: '12px !important', + padding: '5px !important', + }, + '& .fc-day-grid-event.fc-h-event.fc-event.fc-not-start.fc-end': { + opacity: '.65 !important', + marginLeft: '12px !important', + padding: '5px !important', + }, + '& .fc-today': { + background: theme.palette.type === 'dark' ? 'rgba(199,199,199,.1) !important' : 'rgba(0,0,0,.1) !important', + }, + '& .fc-button': { + display: 'inline-block', + position: 'relative', + cursor: 'pointer', + minHeight: '36px', + minWidth: '88px', + lineHeight: '36px', + verticalAlign: 'middle', + alignItems: 'center', + textAlign: 'center', + borderRadius: '2px', + boxSizing: 'border-box', + userSelect: 'none', + outline: 'none', + border: '0', + padding: '0 6px', + margin: '6px 8px', + letterSpacing: '.01em', + background: 'transparent', + color: 'currentColor', + whiteSpace: 'nowrap', + textTransform: 'uppercase', + fontWeight: '500', + fontSize: '14px', + fontStyle: 'inherit', + fontVariant: 'inherit', + fontFamily: 'inherit', + textDecoration: 'none', + overflow: 'hidden', + borderColor: theme.palette.primary.main, + borderWidth: 1, + borderStyle: 'solid', + transition: 'box-shadow .4s cubic-bezier(.25,.8,.25,1),background-color .4s cubic-bezier(.25,.8,.25,1)', + }, + '& .fc-button-active': { + backgroundColor: theme.palette.primary.main + '!important', + }, + '& .fc-button:active': { + backgroundColor: theme.palette.primary.main + '!important', + }, + '& .fc td.fc-axis.fc-time': { + position: 'absolute', + marginTop: -10, + background: theme.palette.background.paper, + zIndex: 1000, + }, + '& .fc td.fc-axis.fc-time span': { + paddingRight: 10, + }, + '& .fc .fc-time-grid-event-inset': { + boxShadow: theme.shadows[5], + }, + '& .fc .fc-mirror': { + opacity: 0.5, + }, + '& .fc .fc-time-grid-event:hover': { + minHeight: 17, + zIndex: '2!important', + }, + '& .fc .fc-time-grid-event[data-has-end="false"]': { + minHeight: 40, + }, + '& .fc .fc-content': { + height: '100%', + }, + '& .fc .fc-content .stop': { + background: theme.palette.type === 'dark' ? 'rgba(255,255,255,.2) !important' : 'rgba(0,0,0,.1) !important', + borderRadius: '6px 0 0 0', + fontSize: '10px', + width: '50%', + padding: '2px 0 1px 0', + textAlign: 'center', + right: 0, + bottom: 0, + position: 'absolute', + display: 'inline-block', + }, + '& .fc .fc-content .stop:hover': { + background: theme.palette.type === 'dark' ? 'rgba(255,255,255,.3) !important' : 'rgba(0,0,0,.2) !important', + }, + '& .fc .fc-content .stop a': { + width: '100%', + display: 'block', + }, + '& .fc .fc-now-indicator': { + zIndex: 5, + }, + '& .fc .ellipsis': { + display: '-webkit-box', + boxOrient: 'vertical', + overflow: 'hidden', + }, + '& .fc .ellipsis-single': { + textOverflow: 'ellipsis', + display: 'inline-block', + width: '100%', + whiteSpace: 'nowrap', + overflow: 'hidden', + }, + '& .fc .__start': { + background: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + border: 0, + borderRadius: '0 0 10px 10px', + textAlign: 'center', + boxShadow: theme.shadows[1], + cursor: 'pointer', + }, + '& .fc .__start:hover': { + background: theme.palette.primary.light, + }, + // tslint:disable-next-line:no-any important breaks it + } as any, + }; +}); + +export const FullCalendarStyling: React.FC = ({children}) => { + const classes = useStyle(); + return
{children}
; +}; diff --git a/ui/src/timespan/colorutils.ts b/ui/src/timespan/colorutils.ts new file mode 100644 index 0000000..4d23f48 --- /dev/null +++ b/ui/src/timespan/colorutils.ts @@ -0,0 +1,28 @@ +// @ts-ignore +import colorHash from 'color-hash'; + +const dark = new colorHash({saturation: 0.35, lightness: 0.35}); +const darkNone = new colorHash({saturation: 0.35, lightness: 0.5}); + +const light = new colorHash({saturation: 0.5, lightness: 0.6}); +const lightNone = new colorHash({saturation: 0.5, lightness: 0.4}); + +export enum ColorMode { + Bold, + None, +} + +const mapping: Record string>> = { + [ColorMode.Bold]: { + dark: (s) => dark.hex(s), + light: (s) => light.hex(s), + }, + [ColorMode.None]: { + dark: (s) => darkNone.hex(s), + light: (s) => lightNone.hex(s), + }, +}; + +export const calculateColor = (s: string, mode: ColorMode, theme: 'light' | 'dark') => { + return mapping[mode][theme](s); +}; diff --git a/ui/src/timespan/timespanutils.ts b/ui/src/timespan/timespanutils.ts new file mode 100644 index 0000000..5a39903 --- /dev/null +++ b/ui/src/timespan/timespanutils.ts @@ -0,0 +1,69 @@ +import {Trackers_timers} from '../gql/__generated__/Trackers'; +import {Tags_tags} from '../gql/__generated__/Tags'; +import {toTagSelectorEntry} from '../tag/tagSelectorEntry'; +import moment from 'moment'; +import {TimeSpanProps} from './TimeSpan'; +import {TimeSpans_timeSpans_timeSpans} from '../gql/__generated__/TimeSpans'; + +export const toTimeSpanProps = (timers: Trackers_timers[], tags: Tags_tags[]): TimeSpanProps[] => { + return [...timers].map((timer) => { + const tagEntries = toTagSelectorEntry(tags, timer.tags || []); + const range: TimeSpanProps['range'] = {from: moment.parseZone(timer.start)}; + if (timer.end) { + range.to = moment.parseZone(timer.end); + } + return { + id: timer.id, + range: { + ...range, + oldFrom: timer.oldStart ? moment(timer.oldStart) : undefined, + }, + initialTags: tagEntries, + note: timer.note, + }; + }); +}; + +type GroupedByIndex = Record; +const group = (startOfTomorrow: moment.Moment, startOfToday: moment.Moment, startOfYesterday: moment.Moment) => ( + a: GroupedByIndex, + current: TimeSpans_timeSpans_timeSpans +): GroupedByIndex => { + const startTime = moment(current.oldStart || current.start); + let date = `${startTime.format('dddd')}, ${startTime.format('LL')}`; + if (startTime.isBetween(startOfToday, startOfTomorrow)) { + date = `${date} (today)`; + } else if (startTime.isBetween(startOfYesterday, startOfToday)) { + date = `${date} (yesterday)`; + } + a[date] = [...(a[date] || []), current]; + return a; +}; + +export type GroupedTimeSpanProps = Array<{key: string; timeSpans: TimeSpanProps[]}>; + +export const toGroupedTimeSpanProps = ( + timeSpans: TimeSpans_timeSpans_timeSpans[], + tags: Tags_tags[], + now: moment.Moment +): GroupedTimeSpanProps => { + const datesWithTimeSpans: GroupedByIndex = timeSpans.reduce( + group( + moment(now) + .add(1, 'day') + .startOf('day'), + moment(now).startOf('day'), + moment(now) + .subtract(1, 'day') + .startOf('day') + ), + {} + ); + return Object.keys(datesWithTimeSpans).map((key) => { + const groupedTimeSpans = datesWithTimeSpans[key]; + return { + key, + timeSpans: toTimeSpanProps(groupedTimeSpans, tags), + }; + }); +}; diff --git a/ui/src/timespan/timeutils.ts b/ui/src/timespan/timeutils.ts new file mode 100644 index 0000000..24876ba --- /dev/null +++ b/ui/src/timespan/timeutils.ts @@ -0,0 +1,24 @@ +import moment from 'moment-timezone'; +import prettyMs from 'pretty-ms'; + +export const timeRunning = (date: moment.Moment, now: moment.Moment) => { + const d = inUserTz(now).unix() - inUserTz(date).unix(); + return prettyMs(d * 1000, {unitCount: 2}).substring(1); +}; + +export const timeRunningCalendar = (date: moment.Moment, now: moment.Moment) => { + const d = inUserTz(now).unix() - inUserTz(date).unix(); + + if (d < 60) { + return '<1m'; + } + + return prettyMs(d * 1000, {unitCount: d < 60 * 24 ? 1 : 2}).substring(1); +}; + +export const uglyConvertToLocalTime = (m: moment.Moment): moment.Moment => { + const withoutTimeZone: string = m.format('YYYY-MM-DDTHH:mm:ss'); + return moment(withoutTimeZone); +}; + +export const inUserTz = (m: moment.Moment): moment.Moment => m.tz(moment.tz.guess()); diff --git a/ui/src/user/AddUserDialog.tsx b/ui/src/user/AddUserDialog.tsx new file mode 100644 index 0000000..b89a5b1 --- /dev/null +++ b/ui/src/user/AddUserDialog.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import * as gqlUser from '../gql/user'; +import {useMutation} from '@apollo/react-hooks'; +import {useSnackbar} from 'notistack'; +import {handleError} from '../utils/errors'; +import {Checkbox} from '@material-ui/core'; +import {CreateUser, CreateUserVariables} from '../gql/__generated__/CreateUser'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; + +interface AddTagDialogProps { + open: boolean; + close: () => void; +} + +export const AddUserDialog: React.FC = ({close, open}) => { + const [name, setName] = React.useState(''); + const [pass, setPass] = React.useState(''); + const [admin, setAdmin] = React.useState(false); + const {enqueueSnackbar} = useSnackbar(); + + const [addUser] = useMutation(gqlUser.CreateUser, { + refetchQueries: [{query: gqlUser.Users}], + }); + const submit = (e: React.FormEvent) => { + e.preventDefault(); + addUser({variables: {name, pass, admin}}) + .then(() => { + enqueueSnackbar('User created', {variant: 'success'}); + close(); + }) + .catch(handleError('Create User', enqueueSnackbar)); + }; + + return ( + +
+ Create User + + setName(e.target.value)} + /> + setPass(e.target.value)} + /> + setAdmin(e.target.checked)}> + Admin + + } + label="Admin" + /> + + + + + +
+
+ ); +}; diff --git a/ui/src/user/UsersPage.tsx b/ui/src/user/UsersPage.tsx new file mode 100644 index 0000000..7d4d9b8 --- /dev/null +++ b/ui/src/user/UsersPage.tsx @@ -0,0 +1,177 @@ +import * as React from 'react'; +import Paper from '@material-ui/core/Paper'; +import {useMutation, useQuery} from '@apollo/react-hooks'; +import * as gqlUser from '../gql/user'; +import {CenteredSpinner} from '../common/CenteredSpinner'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import DeleteIcon from '@material-ui/icons/Delete'; +import EditIcon from '@material-ui/icons/Edit'; +import DoneIcon from '@material-ui/icons/Done'; +import CloseIcon from '@material-ui/icons/Close'; +import IconButton from '@material-ui/core/IconButton'; +import {useSnackbar} from 'notistack'; +import {TextField} from '@material-ui/core'; +import {Users} from '../gql/__generated__/Users'; +import {RemoveUser, RemoveUserVariables} from '../gql/__generated__/RemoveUser'; +import {UpdateUser, UpdateUserVariables} from '../gql/__generated__/UpdateUser'; +import Checkbox from '@material-ui/core/Checkbox'; +import Button from '@material-ui/core/Button'; +import {AddUserDialog} from './AddUserDialog'; +import makeStyles from '@material-ui/core/styles/makeStyles'; +import {ConfirmDialog} from '../common/ConfirmDialog'; + +const useStyles = makeStyles((theme) => ({ + root: { + ...theme.mixins.gutters(), + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(3), + textAlign: 'center', + maxWidth: 800, + margin: '0 auto', + }, +})); + +const NoEdit = [-1, '', '', false] as const; + +export const UsersPage = () => { + const classes = useStyles(); + const {data, loading} = useQuery(gqlUser.Users); + const [addUser, setAddUser] = React.useState(false); + const refetch = {refetchQueries: [{query: gqlUser.Users}, {query: gqlUser.CurrentUser}]}; + const {enqueueSnackbar} = useSnackbar(); + const [removeUserConfirmation, setRemoveUserConfirmation] = React.useState(false); + const [removeUser] = useMutation(gqlUser.RemoveUser, refetch); + const [[editId, editName, editPass, editAdmin], setEditing] = React.useState>( + NoEdit + ); + const [updateUser] = useMutation(gqlUser.UpdateUser, refetch); + if (loading || !data || !data.currentUser || !data.users) { + return ; + } + const onClickDelete = () => { + if (removeUserConfirmation) { + removeUser({variables: {id: removeUserConfirmation[0]}}).then(() => + enqueueSnackbar('user deleted', {variant: 'success'}) + ); + } + }; + + const users = data.users.map((user) => { + const onClickSubmit = () => { + setEditing(NoEdit); + updateUser({ + variables: { + id: editId, + name: editName, + admin: editAdmin, + pass: editPass || undefined, + }, + }).then(() => enqueueSnackbar('user edited', {variant: 'success'})); + }; + const isCurrent = user.id === data.currentUser!.id; + const isEdited = editId === user.id; + return ( + + {user.id} + + {isEdited ? ( + setEditing([editId, e.target.value, '', editAdmin])} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onClickSubmit(); + } + }} + onSubmit={onClickSubmit} + /> + ) : ( + user.name + (isCurrent ? ' (current)' : '') + )} + + + {isEdited ? ( + setEditing([editId, editName, e.target.value, editAdmin])} + /> + ) : ( + '********' + )} + + + {isEdited ? ( + setEditing([editId, editName, '', e.target.checked])} /> + ) : user.admin ? ( + 'Yes' + ) : ( + 'No' + )} + + + {isEdited ? ( + <> + + + + setEditing(NoEdit)} title="Cancel"> + + + + ) : ( + <> + setEditing([user.id, user.name, '', user.admin])} title="Edit"> + + + setRemoveUserConfirmation([user.id, user.name])} title="Delete"> + + + + )} + + + ); + }); + + return ( + + + {addUser && setAddUser(false)} />} + + + + ID + Name + Password + Admin + + + + {users} +
+ {removeUserConfirmation ? ( + setRemoveUserConfirmation(false)} + fOnSubmit={onClickDelete}> + This operation cannot be undone. Deleting the user includes deleting dashboards, time spans, tags and + devices of that user. + + ) : null} +
+ ); +}; diff --git a/ui/src/utils/errors.tsx b/ui/src/utils/errors.tsx new file mode 100644 index 0000000..75e2fd8 --- /dev/null +++ b/ui/src/utils/errors.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import {withSnackbarProps} from 'notistack'; +import {ApolloError} from 'apollo-boost'; + +export const handleError = (prefix: string, enqueue: withSnackbarProps['enqueueSnackbar']): ((error: ApolloError) => void) => { + return (error) => { + error.graphQLErrors.forEach((gqlError) => { + enqueue(`${prefix}: ${gqlError.message}`, {variant: 'warning'}); + }); + }; +}; + +export const useError = (timeout = 1000): [boolean, string, (s: string) => void] => { + const [error, setError] = React.useState(''); + const [active, setActive] = React.useState(false); + + React.useLayoutEffect(() => { + if (!active) { + return; + } + const handle = setTimeout(() => { + setActive(false); + }, timeout); + return () => clearTimeout(handle); + }, [active, timeout]); + + return [ + active, + error, + (message: string) => { + setError(message); + setActive(true); + }, + ]; +}; diff --git a/ui/src/utils/hooks.ts b/ui/src/utils/hooks.ts new file mode 100644 index 0000000..0d6e816 --- /dev/null +++ b/ui/src/utils/hooks.ts @@ -0,0 +1,20 @@ +import * as React from 'react'; +import useTimeout from '@rooks/use-timeout'; + +export const useStateAndDelegateWithDelayOnChange: ( + initialState: T, + delegate: React.Dispatch>, + delay?: number +) => [T, React.Dispatch>] = (initialState, delegate, delay = 50) => { + const [value, setValue] = React.useState(initialState); + const {start} = useTimeout(() => delegate(value), delay); + return [ + value, + (newValue) => { + if (value !== newValue) { + start(); + } + setValue(newValue); + }, + ]; +}; diff --git a/ui/src/utils/never.ts b/ui/src/utils/never.ts new file mode 100644 index 0000000..6d433ad --- /dev/null +++ b/ui/src/utils/never.ts @@ -0,0 +1,3 @@ +export const expectNever = (value: never): never => { + throw new Error('expected never but was ' + value); +}; diff --git a/ui/src/utils/range.ts b/ui/src/utils/range.ts new file mode 100644 index 0000000..83d3290 --- /dev/null +++ b/ui/src/utils/range.ts @@ -0,0 +1,16 @@ +export interface Range { + from: string; + to: string; +} + +export const findRange = (selection: {range: Range | null; rangeId: number | null}, ranges: Record): Range => { + if (selection.rangeId !== null) { + return exclusiveRange(ranges[selection.rangeId]); + } + if (selection.range === null) { + throw new Error('expected rangeId or range to be non null'); + } + return exclusiveRange(selection.range); +}; + +export const exclusiveRange = (range: Range) => ({from: range.from, to: range.to}); diff --git a/ui/src/utils/strip.ts b/ui/src/utils/strip.ts new file mode 100644 index 0000000..518f208 --- /dev/null +++ b/ui/src/utils/strip.ts @@ -0,0 +1,22 @@ +export const stripTypename = (value: T): T => { + if (value === null || value === undefined) { + return value; + } + + if (Array.isArray(value)) { + // tslint:disable-next-line:no-any + return (value as any).map((x: any) => stripTypename(x)); + } + + if (typeof value !== 'object') { + return value; + } + + Object.values(value).forEach(stripTypename); + if ('__typename' in value) { + // @ts-ignore + delete value.__typename; + } + + return value; +}; diff --git a/ui/src/utils/time.test.ts b/ui/src/utils/time.test.ts new file mode 100644 index 0000000..e215263 --- /dev/null +++ b/ui/src/utils/time.test.ts @@ -0,0 +1,42 @@ +import {isValidDate, parseRelativeTime} from './time'; +import moment from 'moment'; + +moment.updateLocale('en', { + week: { + dow: 1, // monday + doy: moment.localeData('en').firstDayOfYear(), + }, +}); + +it('should test for valid date', () => { + expect(isValidDate('2017-05-05')).toBe(false); + expect(isValidDate('2017-05-05T15:23')).toBe(false); + expect(isValidDate('2017-05-05 15:23')).toBe(true); +}); + +// 2018-10-15 Monday +// 2018-10-22 Monday + +// 2019-10-07 Monday +// 2019-10-14 Monday +// 2019-10-21 Monday + +it('should parse', () => { + expectSuccess(parseRelativeTime('now-1d', 'startOf', moment('2019-10-20T15:55:00'))).toEqual('2019-10-19 15:55:00'); + expectSuccess(parseRelativeTime('now-120s', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-20 15:53:15'); + expectSuccess(parseRelativeTime('now-1d-1h', 'startOf', moment('2019-10-20T15:55:00'))).toEqual('2019-10-19 14:55:00'); + expectSuccess(parseRelativeTime('now/w', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-14 00:00:00'); + expectSuccess(parseRelativeTime('now/w', 'endOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-20 23:59:59'); + expectSuccess(parseRelativeTime('now-1w/w', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-07 00:00:00'); + expectSuccess(parseRelativeTime('now-1y+1w/w', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2018-10-22 00:00:00'); + expectSuccess(parseRelativeTime('now/d+5h', 'startOf', moment('2019-10-20T15:55:00'))).toEqual('2019-10-20 05:00:00'); + expectSuccess(parseRelativeTime('now/y', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-01-01 00:00:00'); +}); + +const expectSuccess = (value: ReturnType) => { + if (value.success) { + return expect(value.value.format('YYYY-MM-DD HH:mm:ss')); + } + expect(value.error).toEqual('no error'); + return expect(''); +}; diff --git a/ui/src/utils/time.ts b/ui/src/utils/time.ts new file mode 100644 index 0000000..e1a78c9 --- /dev/null +++ b/ui/src/utils/time.ts @@ -0,0 +1,142 @@ +import moment from 'moment'; + +interface Success { + success: true; + value: moment.Moment; +} + +interface Failure { + success: false; + error: string; +} + +enum Type { + Operation = 'operation', + Unit = 'unit', + Value = 'value', +} + +enum Operation { + Divide = '/', + Add = '+', + Substract = '-', +} + +enum Unit { + Year = 'y', + Month = 'M', + Week = 'w', + Day = 'd', + Hour = 'h', + Minute = 'm', + Second = 's', +} + +export const parseRelativeTime = (value: string, divide: 'endOf' | 'startOf', nowDate = moment()): Success | Failure => { + if (isValidDate(value)) { + return success(asDate(value)); + } + + if (value.substr(0, 3) === 'now') { + let time = nowDate; + let currentIndex = 'now'.length; + + let expectNext = Type.Operation; + let lastOperation = Operation.Add; + let lastNumber = -1; + while (currentIndex < value.length) { + const currentChar = value.charAt(currentIndex); + switch (expectNext) { + case Type.Operation: + if (!isOperation(currentChar)) { + return failure('Expected one of / + - at index ' + currentIndex + ' but was ' + currentChar); + } + lastOperation = currentChar; + expectNext = currentChar === Operation.Divide ? Type.Unit : Type.Value; + currentIndex++; + break; + case Type.Value: + if (isNaN(parseInt(currentChar, 10))) { + return failure('Expected number at index ' + currentIndex + ' but was ' + currentChar); + } + let valueIndex = currentIndex; + while (!isNaN(parseInt(value.charAt(valueIndex + 1), 10)) && valueIndex + 1 < value.length) { + valueIndex++; + } + lastNumber = parseInt(value.substr(currentIndex, valueIndex), 10); + + expectNext = Type.Unit; + currentIndex = valueIndex + 1; + break; + case Type.Unit: + if (!isUnit(currentChar)) { + return failure( + 'Expected unit (' + Object.values(Unit) + ') at index ' + currentIndex + ' but was ' + currentChar + ); + } + + // tslint:disable-next-line:no-nested-switch + switch (lastOperation) { + case Operation.Divide: + time = time[divide](currentChar); + expectNext = Type.Operation; + break; + case Operation.Add: + time = time.add(lastNumber, currentChar); + expectNext = Type.Operation; + break; + case Operation.Substract: + time = time.subtract(lastNumber, currentChar); + expectNext = Type.Operation; + break; + default: + throw new Error('oops'); + } + + currentIndex++; + break; + default: + throw new Error('oopsie'); + } + } + if (expectNext === Type.Unit) { + return failure('Expected unit at the end but got nothing'); + } + if (expectNext === Type.Value) { + return failure('Expected number at the end but got nothing'); + } + return success(time); + } + + if (value.indexOf('now') !== -1) { + return failure("'now' must be at the start"); + } + + return failure("Expected valid date or 'now' at index 0"); +}; + +export const success = (value: moment.Moment): Success => { + return {success: true, value}; +}; +export const failure = (error: string): Failure => { + return {success: false, error}; +}; + +const isOperation = (char: string): char is Operation => { + return Object.values(Operation).indexOf(char as Operation) !== -1; +}; +const isUnit = (char: string): char is Unit => { + return Object.values(Unit).indexOf(char as Unit) !== -1; +}; + +export const isValidDate = (value: string, format?: string) => { + return asDate(value, format).isValid(); +}; + +export const asDate = (value: string, format = 'YYYY-MM-DD HH:mm') => { + return moment(value, format, true); +}; +export const isSameDate = (from: moment.Moment, to?: moment.Moment): boolean => { + const fromString = from.format('YYYYMMDD'); + return to === undefined || fromString === to.format('YYYYMMDD'); +}; diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..3cc389d --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "alwaysStrict": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "suppressImplicitAnyIndexErrors": false, + "strictPropertyInitialization": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strictFunctionTypes": true, + "jsx": "preserve" + }, + "include": [ + "src" + ] +} diff --git a/ui/tslint.json b/ui/tslint.json new file mode 100644 index 0000000..731bc17 --- /dev/null +++ b/ui/tslint.json @@ -0,0 +1,120 @@ +{ + "extends": "tslint-sonarts", + "rules": { + "array-type": [true, "array-simple"], + "arrow-return-shorthand": true, + "ban": [true, + {"name": "Array", "message": "tsstyle#array-constructor"} + ], + "ban-types": [true, + ["Object", "Use {} instead."], + ["String", "Use 'string' instead."], + ["Number", "Use 'number' instead."], + ["Boolean", "Use 'boolean' instead."], + ["Function", "Use (..) => .. instead."] + ], + "class-name": true, + "curly": [true], + "forin": true, + "object-literal-shorthand": true, + "label-position": true, + "member-access": [true], + "new-parens": true, + "no-angle-bracket-type-assertion": true, + "no-any": true, + "no-arg": true, + "no-conditional-assignment": true, + "no-construct": true, + "no-shadowed-variable": true, + "prefer-object-spread": true, + "no-debugger": true, + "no-default-export": true, + "no-duplicate-variable": true, + "no-inferrable-types": true, + "no-namespace": [true, "allow-declarations"], + "cyclomatic-complexity": [true, 20], + "no-duplicate-imports": true, + "no-reference": true, + "no-string-throw": true, + "no-unused-expression": true, + "no-var-keyword": true, + "object-literal-shorthand": true, + "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], + "prefer-const": true, + "radix": true, + "semicolon": [true, "always", "ignore-bound-class-methods"], + "switch-default": true, + "triple-equals": [true, "allow-null-check"], + "use-isnan": true, + "variable-name": [ + true, + "check-format", + "ban-keywords", + "allow-pascal-case", + "allow-leading-underscore", + "allow-trailing-underscore" + ], + "cognitive-complexity": [true, 20], + "mccabe-complexity": [true, 20], + "no-duplicate-string": false, + "no-nested-incdec": false, + "no-all-duplicated-branches": true, + "consecutive-overloads": true, + "max-union-size": false, + "no-accessor-field-mismatch": true, + "no-array-delete": true, + "no-big-function": false, + "no-case-with-or": true, + "no-collection-size-mischeck": true, + "no-commented-code": true, + "no-dead-store": true, + "no-duplicate-in-composite": true, + "no-duplicated-branches": true, + "no-element-overwrite": true, + "no-empty-destructuring": true, + "no-empty-nested-blocks": true, + "no-extra-semicolon": true, + "no-gratuitous-expressions": true, + "no-hardcoded-credentials": true, + "no-identical-conditions": true, + "no-identical-expressions": true, + "no-identical-functions": true, + "no-ignored-initial-value": true, + "no-ignored-return": true, + "no-in-misuse": true, + "no-inconsistent-return": true, + "no-misleading-array-reverse": true, + "no-misspelled-operator": true, + "no-multiline-string-literals": true, + "no-nested-template-literals": true, + "no-redundant-boolean": true, + "no-redundant-jump": true, + "no-redundant-parentheses": true, + "no-return-type-any": true, + "no-same-line-conditional": true, + "no-self-assignment": true, + "no-small-switch": true, + "no-statements-same-line": true, + "no-unconditional-jump": true, + "no-undefined-argument": true, + "no-unenclosed-multiline-block": true, + "no-unthrown-error": true, + "no-unused-array": true, + "no-use-of-empty-return-value": true, + "no-useless-cast": true, + "no-useless-increment": true, + "no-useless-intersection": true, + "no-variable-usage-before-declaration": true, + "parameters-max-number": true, + "prefer-default-last": true, + "prefer-immediate-return": true, + "prefer-promise-shorthand": true, + "use-primitive-type": true, + "use-type-alias": true + }, + "linterOptions": { + "exclude": [ + "node_modules/**/*.ts" + ] + } +} diff --git a/ui/yarn.lock b/ui/yarn.lock new file mode 100644 index 0000000..a82a51c --- /dev/null +++ b/ui/yarn.lock @@ -0,0 +1,13130 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@apollo/federation@0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@apollo/federation/-/federation-0.10.2.tgz#6ead14e9d25b7a87d85fc0188fd5ec7fd63592fe" + integrity sha512-N+JoD7YraGAgDJq1GTkAzRRPUA+FgclFgAUcUDng9ICA4N147nuGUrbrpQdgLnE59nCLJAhcFvOgQRs177XMWw== + dependencies: + apollo-env "^0.5.1" + apollo-graphql "^0.3.4" + apollo-server-env "^2.4.3" + lodash.xorby "^4.7.0" + +"@apollo/react-common@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@apollo/react-common/-/react-common-3.1.3.tgz#ddc34f6403f55d47c0da147fd4756dfd7c73dac5" + integrity sha512-Q7ZjDOeqjJf/AOGxUMdGxKF+JVClRXrYBGVq+SuVFqANRpd68MxtVV2OjCWavsFAN0eqYnRqRUrl7vtUCiJqeg== + dependencies: + ts-invariant "^0.4.4" + tslib "^1.10.0" + +"@apollo/react-components@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@apollo/react-components/-/react-components-3.1.3.tgz#8f6726847cd9b0eb4b22586b1a038d29aa8b1da4" + integrity sha512-H0l2JKDQMz+LkM93QK7j3ThbNXkWQCduN3s3eKxFN3Rdg7rXsrikJWvx2wQ868jmqy0VhwJbS1vYdRLdh114uQ== + dependencies: + "@apollo/react-common" "^3.1.3" + "@apollo/react-hooks" "^3.1.3" + prop-types "^15.7.2" + ts-invariant "^0.4.4" + tslib "^1.10.0" + +"@apollo/react-hoc@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@apollo/react-hoc/-/react-hoc-3.1.3.tgz#5742ee74f57611058f5ea1f966c38fc6429dda7b" + integrity sha512-oCPma0uBVPTcYTR5sOvtMbpaWll4xDBvYfKr6YkDorUcQVeNzFu1LK1kmQjJP64bKsaziKYji5ibFaeCnVptmA== + dependencies: + "@apollo/react-common" "^3.1.3" + "@apollo/react-components" "^3.1.3" + hoist-non-react-statics "^3.3.0" + ts-invariant "^0.4.4" + tslib "^1.10.0" + +"@apollo/react-hooks@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@apollo/react-hooks/-/react-hooks-3.1.3.tgz#ad42c7af78e81fee0f30e53242640410d5bd0293" + integrity sha512-reIRO9xKdfi+B4gT/o/hnXuopUnm7WED/ru8VQydPw+C/KG/05Ssg1ZdxFKHa3oxwiTUIDnevtccIH35POanbA== + dependencies: + "@apollo/react-common" "^3.1.3" + "@wry/equality" "^0.1.9" + ts-invariant "^0.4.4" + tslib "^1.10.0" + +"@apollo/react-ssr@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@apollo/react-ssr/-/react-ssr-3.1.3.tgz#0791280d5b735f42f87dbfe849564e78843045bc" + integrity sha512-fUTmEYHxSTX1GA43B8vICxXXplpcEBnDwn0IgdAc3eG0p2YK97ZrJDRFCJ5vD7fyDZsrYhMf+rAI3sd+H2SS+A== + dependencies: + "@apollo/react-common" "^3.1.3" + "@apollo/react-hooks" "^3.1.3" + tslib "^1.10.0" + +"@apollographql/apollo-tools@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.4.0.tgz#8a1a0ab7a0bb12ccc03b72e4a104cfa5d969fd5f" + integrity sha512-7wEO+S+zgz/wVe3ilFQqICufRBYYDSNUkd1V03JWvXuSydbYq2SM5EgvWmFF+04iadt+aQ0XCCsRzCzRPQODfQ== + dependencies: + apollo-env "0.5.1" + +"@apollographql/graphql-language-service-interface@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@apollographql/graphql-language-service-interface/-/graphql-language-service-interface-2.0.2.tgz#0e793636eca3d2ee0f818602d52fb5dab9edc0e3" + integrity sha512-28wePK0hlIVjgmvMXMAUq8qRSjz9O+6lqFp4PzOTHtfJfSsjVe9EfjF98zTpHsTgT3HcOxmbqDZZy8jlXtOqEA== + dependencies: + "@apollographql/graphql-language-service-parser" "^2.0.0" + "@apollographql/graphql-language-service-types" "^2.0.0" + "@apollographql/graphql-language-service-utils" "^2.0.2" + +"@apollographql/graphql-language-service-parser@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@apollographql/graphql-language-service-parser/-/graphql-language-service-parser-2.0.2.tgz#50cb7a6c3e331eae09f6de13101da688dab261f1" + integrity sha512-rpTPrEJu1PMaRQxz5P8BZWsixNNhYloS0H0dwTxNBuE3qctbARvR7o8UCKLsmKgTbo+cz3T3a6IAsWlkHgMWGg== + dependencies: + "@apollographql/graphql-language-service-types" "^2.0.0" + +"@apollographql/graphql-language-service-types@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@apollographql/graphql-language-service-types/-/graphql-language-service-types-2.0.2.tgz#1034e47eb7479129959c1bed2ee12d874aab5cab" + integrity sha512-vE+Dz8pG+Xa1Z2nMl82LoO66lQ6JqBUjaXqLDvS3eMjvA3N4hf+YUDOWfPdNZ0zjhHhHXzUIIZCkax6bXfFbzQ== + +"@apollographql/graphql-language-service-utils@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@apollographql/graphql-language-service-utils/-/graphql-language-service-utils-2.0.2.tgz#aa552c31de16172433bbdbc03914585caaca1d03" + integrity sha512-fDj5rWlTi/czvUS5t7V7I45Ai6bOO3Z7JARYj21Y2xxfbRGtJi6h8FvLX0N/EbzQgo/fiZc/HAhtfwn+OCjD7A== + dependencies: + "@apollographql/graphql-language-service-types" "^2.0.0" + +"@babel/code-frame@7.5.5", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" + integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== + dependencies: + "@babel/highlight" "^7.0.0" + +"@babel/core@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.0.tgz#9b00f73554edd67bebc86df8303ef678be3d7b48" + integrity sha512-FuRhDRtsd6IptKpHXAa+4WPZYY2ZzgowkbLBecEDDSje1X/apG7jQM33or3NdOmjXBKWGOg4JmSiRfUfuTtHXw== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.6.0" + "@babel/helpers" "^7.6.0" + "@babel/parser" "^7.6.0" + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.0" + "@babel/types" "^7.6.0" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/core@^7.1.0", "@babel/core@^7.4.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30" + integrity sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.5.5" + "@babel/helpers" "^7.5.5" + "@babel/parser" "^7.5.5" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.5.5" + "@babel/types" "^7.5.5" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@7.6.4": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.4.tgz#a4f8437287bf9671b07f483b76e3bb731bc97671" + integrity sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w== + dependencies: + "@babel/types" "^7.6.3" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + +"@babel/generator@^7.4.0", "@babel/generator@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.5.tgz#873a7f936a3c89491b43536d12245b626664e3cf" + integrity sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ== + dependencies: + "@babel/types" "^7.5.5" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@babel/generator@^7.6.0", "@babel/generator@^7.7.2": + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.2.tgz#2f4852d04131a5e17ea4f6645488b5da66ebf3af" + integrity sha512-WthSArvAjYLz4TcbKOi88me+KmDJdKSlfwwN8CnUYn9jBkzhq0ZEPuBfkAWIvjJ3AdEV1Cf/+eSQTnp3IDJKlQ== + dependencies: + "@babel/types" "^7.7.2" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + +"@babel/helper-annotate-as-pure@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" + integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz#6b69628dfe4087798e0c4ed98e3d4a6b2fbd2f5f" + integrity sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-builder-react-jsx@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz#a1ac95a5d2b3e88ae5e54846bf462eeb81b318a4" + integrity sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw== + dependencies: + "@babel/types" "^7.3.0" + esutils "^2.0.0" + +"@babel/helper-call-delegate@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz#87c1f8ca19ad552a736a7a27b1c1fcf8b1ff1f43" + integrity sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ== + dependencies: + "@babel/helper-hoist-variables" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + +"@babel/helper-create-class-features-plugin@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.5.tgz#401f302c8ddbc0edd36f7c6b2887d8fa1122e5a4" + integrity sha512-ZsxkyYiRA7Bg+ZTRpPvB6AbOFKTFFK4LrvTet8lInm0V468MWCaSYJE+I7v2z2r8KNLtYiV+K5kTCnR7dvyZjg== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-member-expression-to-functions" "^7.5.5" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.5.5" + "@babel/helper-split-export-declaration" "^7.4.4" + +"@babel/helper-create-class-features-plugin@^7.6.0", "@babel/helper-create-class-features-plugin@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.7.0.tgz#bcdc223abbfdd386f94196ae2544987f8df775e8" + integrity sha512-MZiB5qvTWoyiFOgootmRSDV1udjIqJW/8lmxgzKq6oDqxdmHUjeP2ZUOmgHdYjmUVNABqRrHjYAYRvj8Eox/UA== + dependencies: + "@babel/helper-function-name" "^7.7.0" + "@babel/helper-member-expression-to-functions" "^7.7.0" + "@babel/helper-optimise-call-expression" "^7.7.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.7.0" + "@babel/helper-split-export-declaration" "^7.7.0" + +"@babel/helper-create-regexp-features-plugin@^7.7.0": + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.7.2.tgz#6f20443778c8fce2af2ff4206284afc0ced65db6" + integrity sha512-pAil/ZixjTlrzNpjx+l/C/wJk002Wo7XbbZ8oujH/AoJ3Juv0iN/UTcPUHXKMFLqsfS0Hy6Aow8M31brUYBlQQ== + dependencies: + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.6.0" + +"@babel/helper-define-map@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.5.5.tgz#3dec32c2046f37e09b28c93eb0b103fd2a25d369" + integrity sha512-fTfxx7i0B5NJqvUOBBGREnrqbTxRh7zinBANpZXAVDlsZxYdclDp467G1sQ8VZYMnAURY3RpBUAgOYT9GfzHBg== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/types" "^7.5.5" + lodash "^4.17.13" + +"@babel/helper-explode-assignable-expression@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz#537fa13f6f1674df745b0c00ec8fe4e99681c8f6" + integrity sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA== + dependencies: + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-function-name@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" + integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== + dependencies: + "@babel/helper-get-function-arity" "^7.0.0" + "@babel/template" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-function-name@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.7.0.tgz#44a5ad151cfff8ed2599c91682dda2ec2c8430a3" + integrity sha512-tDsJgMUAP00Ugv8O2aGEua5I2apkaQO7lBGUq1ocwN3G23JE5Dcq0uh3GvFTChPa4b40AWiAsLvCZOA2rdnQ7Q== + dependencies: + "@babel/helper-get-function-arity" "^7.7.0" + "@babel/template" "^7.7.0" + "@babel/types" "^7.7.0" + +"@babel/helper-get-function-arity@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" + integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-get-function-arity@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.0.tgz#c604886bc97287a1d1398092bc666bc3d7d7aa2d" + integrity sha512-tLdojOTz4vWcEnHWHCuPN5P85JLZWbm5Fx5ZsMEMPhF3Uoe3O7awrbM2nQ04bDOUToH/2tH/ezKEOR8zEYzqyw== + dependencies: + "@babel/types" "^7.7.0" + +"@babel/helper-hoist-variables@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz#0298b5f25c8c09c53102d52ac4a98f773eb2850a" + integrity sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w== + dependencies: + "@babel/types" "^7.4.4" + +"@babel/helper-member-expression-to-functions@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz#1fb5b8ec4453a93c439ee9fe3aeea4a84b76b590" + integrity sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA== + dependencies: + "@babel/types" "^7.5.5" + +"@babel/helper-member-expression-to-functions@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.7.0.tgz#472b93003a57071f95a541ea6c2b098398bcad8a" + integrity sha512-QaCZLO2RtBcmvO/ekOLp8p7R5X2JriKRizeDpm5ChATAFWrrYDcDxPuCIBXKyBjY+i1vYSdcUTMIb8psfxHDPA== + dependencies: + "@babel/types" "^7.7.0" + +"@babel/helper-module-imports@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d" + integrity sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-module-imports@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.7.0.tgz#99c095889466e5f7b6d66d98dffc58baaf42654d" + integrity sha512-Dv3hLKIC1jyfTkClvyEkYP2OlkzNvWs5+Q8WgPbxM5LMeorons7iPP91JM+DU7tRbhqA1ZeooPaMFvQrn23RHw== + dependencies: + "@babel/types" "^7.7.0" + +"@babel/helper-module-transforms@^7.1.0", "@babel/helper-module-transforms@^7.4.4": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz#f84ff8a09038dcbca1fd4355661a500937165b4a" + integrity sha512-jBeCvETKuJqeiaCdyaheF40aXnnU1+wkSiUs/IQg3tB85up1LyL8x77ClY8qJpuRJUcXQo+ZtdNESmZl4j56Pw== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-simple-access" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/template" "^7.4.4" + "@babel/types" "^7.5.5" + lodash "^4.17.13" + +"@babel/helper-module-transforms@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.7.0.tgz#154a69f0c5b8fd4d39e49750ff7ac4faa3f36786" + integrity sha512-rXEefBuheUYQyX4WjV19tuknrJFwyKw0HgzRwbkyTbB+Dshlq7eqkWbyjzToLrMZk/5wKVKdWFluiAsVkHXvuQ== + dependencies: + "@babel/helper-module-imports" "^7.7.0" + "@babel/helper-simple-access" "^7.7.0" + "@babel/helper-split-export-declaration" "^7.7.0" + "@babel/template" "^7.7.0" + "@babel/types" "^7.7.0" + lodash "^4.17.13" + +"@babel/helper-optimise-call-expression@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz#a2920c5702b073c15de51106200aa8cad20497d5" + integrity sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-optimise-call-expression@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.0.tgz#4f66a216116a66164135dc618c5d8b7a959f9365" + integrity sha512-48TeqmbazjNU/65niiiJIJRc5JozB8acui1OS7bSd6PgxfuovWsvjfWSzlgx+gPFdVveNzUdpdIg5l56Pl5jqg== + dependencies: + "@babel/types" "^7.7.0" + +"@babel/helper-plugin-utils@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" + integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== + +"@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.5.5.tgz#0aa6824f7100a2e0e89c1527c23936c152cab351" + integrity sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw== + dependencies: + lodash "^4.17.13" + +"@babel/helper-remap-async-to-generator@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f" + integrity sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-wrap-function" "^7.1.0" + "@babel/template" "^7.1.0" + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-replace-supers@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz#f84ce43df031222d2bad068d2626cb5799c34bc2" + integrity sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.5.5" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/traverse" "^7.5.5" + "@babel/types" "^7.5.5" + +"@babel/helper-replace-supers@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.7.0.tgz#d5365c8667fe7cbd13b8ddddceb9bd7f2b387512" + integrity sha512-5ALYEul5V8xNdxEeWvRsBzLMxQksT7MaStpxjJf9KsnLxpAKBtfw5NeMKZJSYDa0lKdOcy0g+JT/f5mPSulUgg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.7.0" + "@babel/helper-optimise-call-expression" "^7.7.0" + "@babel/traverse" "^7.7.0" + "@babel/types" "^7.7.0" + +"@babel/helper-simple-access@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c" + integrity sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w== + dependencies: + "@babel/template" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-simple-access@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.7.0.tgz#97a8b6c52105d76031b86237dc1852b44837243d" + integrity sha512-AJ7IZD7Eem3zZRuj5JtzFAptBw7pMlS3y8Qv09vaBWoFsle0d1kAn5Wq6Q9MyBXITPOKnxwkZKoAm4bopmv26g== + dependencies: + "@babel/template" "^7.7.0" + "@babel/types" "^7.7.0" + +"@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== + dependencies: + "@babel/types" "^7.4.4" + +"@babel/helper-split-export-declaration@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.0.tgz#1365e74ea6c614deeb56ebffabd71006a0eb2300" + integrity sha512-HgYSI8rH08neWlAH3CcdkFg9qX9YsZysZI5GD8LjhQib/mM0jGOZOVkoUiiV2Hu978fRtjtsGsW6w0pKHUWtqA== + dependencies: + "@babel/types" "^7.7.0" + +"@babel/helper-wrap-function@^7.1.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" + integrity sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/template" "^7.1.0" + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.2.0" + +"@babel/helpers@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.5.tgz#63908d2a73942229d1e6685bc2a0e730dde3b75e" + integrity sha512-nRq2BUhxZFnfEn/ciJuhklHvFOqjJUD5wpx+1bxUF2axL9C+v4DE/dmp5sT2dKnpOs4orZWzpAZqlCy8QqE/7g== + dependencies: + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.5.5" + "@babel/types" "^7.5.5" + +"@babel/helpers@^7.6.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.7.0.tgz#359bb5ac3b4726f7c1fde0ec75f64b3f4275d60b" + integrity sha512-VnNwL4YOhbejHb7x/b5F39Zdg5vIQpUUNzJwx0ww1EcVRt41bbGRZWhAURrfY32T5zTT3qwNOQFWpn+P0i0a2g== + dependencies: + "@babel/template" "^7.7.0" + "@babel/traverse" "^7.7.0" + "@babel/types" "^7.7.0" + +"@babel/highlight@^7.0.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" + integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" + integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== + +"@babel/parser@^7.1.3", "@babel/parser@^7.6.0", "@babel/parser@^7.7.0", "@babel/parser@^7.7.2": + version "7.7.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.3.tgz#5fad457c2529de476a248f75b0f090b3060af043" + integrity sha512-bqv+iCo9i+uLVbI0ILzKkvMorqxouI+GbV13ivcARXn9NNEabi2IEz912IgNpT/60BNXac5dgcfjb94NjsF33A== + +"@babel/plugin-proposal-async-generator-functions@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" + integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-remap-async-to-generator" "^7.1.0" + "@babel/plugin-syntax-async-generators" "^7.2.0" + +"@babel/plugin-proposal-class-properties@7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz#a974cfae1e37c3110e71f3c6a2e48b8e71958cd4" + integrity sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.5.5" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-proposal-decorators@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.6.0.tgz#6659d2572a17d70abd68123e89a12a43d90aa30c" + integrity sha512-ZSyYw9trQI50sES6YxREXKu+4b7MAg6Qx2cvyDDYjP2Hpzd3FleOUwC9cqn1+za8d0A2ZU8SHujxFao956efUg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.6.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-decorators" "^7.2.0" + +"@babel/plugin-proposal-dynamic-import@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz#e532202db4838723691b10a67b8ce509e397c506" + integrity sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-dynamic-import" "^7.2.0" + +"@babel/plugin-proposal-json-strings@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" + integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-json-strings" "^7.2.0" + +"@babel/plugin-proposal-object-rest-spread@7.5.5", "@babel/plugin-proposal-object-rest-spread@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.5.tgz#61939744f71ba76a3ae46b5eea18a54c16d22e58" + integrity sha512-F2DxJJSQ7f64FyTVl5cw/9MWn6naXGdk3Q3UhDbFEEHv+EilCPoeRD3Zh/Utx1CJz4uyKlQ4uH+bJPbEhMV7Zw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + +"@babel/plugin-proposal-optional-catch-binding@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5" + integrity sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" + +"@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz#501ffd9826c0b91da22690720722ac7cb1ca9c78" + integrity sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" + +"@babel/plugin-syntax-async-generators@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f" + integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-decorators@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.2.0.tgz#c50b1b957dcc69e4b1127b65e1c33eef61570c1b" + integrity sha512-38QdqVoXdHUQfTpZo3rQwqQdWtCn5tMv4uV6r2RMfTqNBuv4ZBhz79SfaQWKTVmxHjeFv/DnXVC/+agHCklYWA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-dynamic-import@7.2.0", "@babel/plugin-syntax-dynamic-import@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612" + integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-flow@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.2.0.tgz#a765f061f803bc48f240c26f8747faf97c26bf7c" + integrity sha512-r6YMuZDWLtLlu0kqIim5o/3TNRAlWb073HwT3e2nKf9I8IIvOggPrnILYPsrrKilmn/mYEMCf/Z07w3yQJF6dg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-json-strings@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470" + integrity sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-jsx@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz#0b85a3b4bc7cdf4cc4b8bf236335b907ca22e7c7" + integrity sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" + integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz#a94013d6eda8908dfe6a477e7f9eda85656ecf5c" + integrity sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-typescript@^7.2.0": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz#a7cc3f66119a9f7ebe2de5383cce193473d65991" + integrity sha512-dGwbSMA1YhVS8+31CnPR7LB4pcbrzcV99wQzby4uAfrkZPYZlQ7ImwdpzLqi6Z6IL02b8IAL379CaMwo0x5Lag== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-arrow-functions@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" + integrity sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-async-to-generator@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz#89a3848a0166623b5bc481164b5936ab947e887e" + integrity sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-remap-async-to-generator" "^7.1.0" + +"@babel/plugin-transform-block-scoped-functions@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190" + integrity sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-block-scoping@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.5.5.tgz#a35f395e5402822f10d2119f6f8e045e3639a2ce" + integrity sha512-82A3CLRRdYubkG85lKwhZB0WZoHxLGsJdux/cOVaJCJpvYFl1LVzAIFyRsa7CvXqW8rBM4Zf3Bfn8PHt5DP0Sg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + lodash "^4.17.13" + +"@babel/plugin-transform-block-scoping@^7.6.0": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.6.3.tgz#6e854e51fbbaa84351b15d4ddafe342f3a5d542a" + integrity sha512-7hvrg75dubcO3ZI2rjYTzUrEuh1E9IyDEhhB6qfcooxhDA33xx2MasuLVgdxzcP6R/lipAC6n9ub9maNW6RKdw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + lodash "^4.17.13" + +"@babel/plugin-transform-classes@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.5.5.tgz#d094299d9bd680a14a2a0edae38305ad60fb4de9" + integrity sha512-U2htCNK/6e9K7jGyJ++1p5XRU+LJjrwtoiVn9SzRlDT2KubcZ11OOwy3s24TjHxPgxNwonCYP7U2K51uVYCMDg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-define-map" "^7.5.5" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.5.5" + "@babel/helper-split-export-declaration" "^7.4.4" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz#83a7df6a658865b1c8f641d510c6f3af220216da" + integrity sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-destructuring@7.6.0", "@babel/plugin-transform-destructuring@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.6.0.tgz#44bbe08b57f4480094d57d9ffbcd96d309075ba6" + integrity sha512-2bGIS5P1v4+sWTCnKNDZDxbGvEqi0ijeqM/YqHtVGrvG2y0ySgnEEhXErvE9dA0bnIzY9bIzdFK0jFA46ASIIQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-destructuring@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz#f6c09fdfe3f94516ff074fe877db7bc9ef05855a" + integrity sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3" + integrity sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" + +"@babel/plugin-transform-duplicate-keys@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz#c5dbf5106bf84cdf691222c0974c12b1df931853" + integrity sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-exponentiation-operator@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008" + integrity sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-flow-strip-types@7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.4.4.tgz#d267a081f49a8705fc9146de0768c6b58dccd8f7" + integrity sha512-WyVedfeEIILYEaWGAUWzVNyqG4sfsNooMhXWsu/YzOvVGcsnPb5PguysjJqI3t3qiaYj0BR8T2f5njdjTGe44Q== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-flow" "^7.2.0" + +"@babel/plugin-transform-for-of@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz#0267fc735e24c808ba173866c6c4d1440fc3c556" + integrity sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-function-name@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz#e1436116abb0610c2259094848754ac5230922ad" + integrity sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-literals@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz#690353e81f9267dad4fd8cfd77eafa86aba53ea1" + integrity sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-member-expression-literals@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz#fa10aa5c58a2cb6afcf2c9ffa8cb4d8b3d489a2d" + integrity sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-modules-amd@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz#ef00435d46da0a5961aa728a1d2ecff063e4fb91" + integrity sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg== + dependencies: + "@babel/helper-module-transforms" "^7.1.0" + "@babel/helper-plugin-utils" "^7.0.0" + babel-plugin-dynamic-import-node "^2.3.0" + +"@babel/plugin-transform-modules-commonjs@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74" + integrity sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ== + dependencies: + "@babel/helper-module-transforms" "^7.4.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-simple-access" "^7.1.0" + babel-plugin-dynamic-import-node "^2.3.0" + +"@babel/plugin-transform-modules-commonjs@^7.6.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.7.0.tgz#3e5ffb4fd8c947feede69cbe24c9554ab4113fe3" + integrity sha512-KEMyWNNWnjOom8vR/1+d+Ocz/mILZG/eyHHO06OuBQ2aNhxT62fr4y6fGOplRx+CxCSp3IFwesL8WdINfY/3kg== + dependencies: + "@babel/helper-module-transforms" "^7.7.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-simple-access" "^7.7.0" + babel-plugin-dynamic-import-node "^2.3.0" + +"@babel/plugin-transform-modules-systemjs@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz#e75266a13ef94202db2a0620977756f51d52d249" + integrity sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg== + dependencies: + "@babel/helper-hoist-variables" "^7.4.4" + "@babel/helper-plugin-utils" "^7.0.0" + babel-plugin-dynamic-import-node "^2.3.0" + +"@babel/plugin-transform-modules-umd@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae" + integrity sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw== + dependencies: + "@babel/helper-module-transforms" "^7.1.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz#9d269fd28a370258199b4294736813a60bbdd106" + integrity sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg== + dependencies: + regexp-tree "^0.1.6" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.6.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.7.0.tgz#358e6fd869b9a4d8f5cbc79e4ed4fc340e60dcaf" + integrity sha512-+SicSJoKouPctL+j1pqktRVCgy+xAch1hWWTMy13j0IflnyNjaoskj+DwRQFimHbLqO3sq2oN2CXMvXq3Bgapg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.7.0" + +"@babel/plugin-transform-new-target@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz#18d120438b0cc9ee95a47f2c72bc9768fbed60a5" + integrity sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-object-super@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.5.5.tgz#c70021df834073c65eb613b8679cc4a381d1a9f9" + integrity sha512-un1zJQAhSosGFBduPgN/YFNvWVpRuHKU7IHBglLoLZsGmruJPOo6pbInneflUdmq7YvSVqhpPs5zdBvLnteltQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.5.5" + +"@babel/plugin-transform-parameters@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz#7556cf03f318bd2719fe4c922d2d808be5571e16" + integrity sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw== + dependencies: + "@babel/helper-call-delegate" "^7.4.4" + "@babel/helper-get-function-arity" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-property-literals@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz#03e33f653f5b25c4eb572c98b9485055b389e905" + integrity sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-react-constant-elements@^7.0.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.5.0.tgz#4d6ae4033bc38f8a65dfca2b6235c44522a422fc" + integrity sha512-c5Ba8cpybZFp1Izkf2sWGuNjOxoQ32tFgBvvYvwGhi4+9f6vGiSK9Gex4uVuO/Va6YJFu41aAh1MzMjUWkp0IQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-react-display-name@7.2.0", "@babel/plugin-transform-react-display-name@^7.0.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz#ebfaed87834ce8dc4279609a4f0c324c156e3eb0" + integrity sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-react-jsx-self@^7.0.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz#461e21ad9478f1031dd5e276108d027f1b5240ba" + integrity sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@babel/plugin-transform-react-jsx-source@^7.0.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.5.0.tgz#583b10c49cf057e237085bcbd8cc960bd83bd96b" + integrity sha512-58Q+Jsy4IDCZx7kqEZuSDdam/1oW8OdDX8f+Loo6xyxdfg1yF0GE2XNJQSTZCaMol93+FBzpWiPEwtbMloAcPg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@babel/plugin-transform-react-jsx@^7.0.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz#f2cab99026631c767e2745a5368b331cfe8f5290" + integrity sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg== + dependencies: + "@babel/helper-builder-react-jsx" "^7.3.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@babel/plugin-transform-regenerator@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz#629dc82512c55cee01341fb27bdfcb210354680f" + integrity sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA== + dependencies: + regenerator-transform "^0.14.0" + +"@babel/plugin-transform-reserved-words@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz#4792af87c998a49367597d07fedf02636d2e1634" + integrity sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-runtime@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.6.0.tgz#85a3cce402b28586138e368fce20ab3019b9713e" + integrity sha512-Da8tMf7uClzwUm/pnJ1S93m/aRXmoYNDD7TkHua8xBDdaAs54uZpTWvEt6NGwmoVMb9mZbntfTqmG2oSzN/7Vg== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + resolve "^1.8.1" + semver "^5.5.1" + +"@babel/plugin-transform-shorthand-properties@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0" + integrity sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-spread@^7.2.0": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz#3103a9abe22f742b6d406ecd3cd49b774919b406" + integrity sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-sticky-regex@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz#a1e454b5995560a9c1e0d537dfc15061fd2687e1" + integrity sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-regex" "^7.0.0" + +"@babel/plugin-transform-template-literals@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz#9d28fea7bbce637fb7612a0750989d8321d4bcb0" + integrity sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-typeof-symbol@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz#117d2bcec2fbf64b4b59d1f9819894682d29f2b2" + integrity sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-typescript@^7.6.0": + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.7.2.tgz#eb9f14c516b5d36f4d6f3a9d7badae6d0fc313d4" + integrity sha512-UWhDaJRqdPUtdK1s0sKYdoRuqK0NepjZto2UZltvuCgMoMZmdjhgz5hcRokie/3aYEaSz3xvusyoayVaq4PjRg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.7.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-typescript" "^7.2.0" + +"@babel/plugin-transform-unicode-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz#ab4634bb4f14d36728bf5978322b35587787970f" + integrity sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-regex" "^7.4.4" + regexpu-core "^4.5.4" + +"@babel/preset-env@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.0.tgz#aae4141c506100bb2bfaa4ac2a5c12b395619e50" + integrity sha512-1efzxFv/TcPsNXlRhMzRnkBFMeIqBBgzwmZwlFDw5Ubj0AGLeufxugirwZmkkX/ayi3owsSqoQ4fw8LkfK9SYg== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-async-generator-functions" "^7.2.0" + "@babel/plugin-proposal-dynamic-import" "^7.5.0" + "@babel/plugin-proposal-json-strings" "^7.2.0" + "@babel/plugin-proposal-object-rest-spread" "^7.5.5" + "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-syntax-async-generators" "^7.2.0" + "@babel/plugin-syntax-dynamic-import" "^7.2.0" + "@babel/plugin-syntax-json-strings" "^7.2.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" + "@babel/plugin-transform-arrow-functions" "^7.2.0" + "@babel/plugin-transform-async-to-generator" "^7.5.0" + "@babel/plugin-transform-block-scoped-functions" "^7.2.0" + "@babel/plugin-transform-block-scoping" "^7.6.0" + "@babel/plugin-transform-classes" "^7.5.5" + "@babel/plugin-transform-computed-properties" "^7.2.0" + "@babel/plugin-transform-destructuring" "^7.6.0" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/plugin-transform-duplicate-keys" "^7.5.0" + "@babel/plugin-transform-exponentiation-operator" "^7.2.0" + "@babel/plugin-transform-for-of" "^7.4.4" + "@babel/plugin-transform-function-name" "^7.4.4" + "@babel/plugin-transform-literals" "^7.2.0" + "@babel/plugin-transform-member-expression-literals" "^7.2.0" + "@babel/plugin-transform-modules-amd" "^7.5.0" + "@babel/plugin-transform-modules-commonjs" "^7.6.0" + "@babel/plugin-transform-modules-systemjs" "^7.5.0" + "@babel/plugin-transform-modules-umd" "^7.2.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.6.0" + "@babel/plugin-transform-new-target" "^7.4.4" + "@babel/plugin-transform-object-super" "^7.5.5" + "@babel/plugin-transform-parameters" "^7.4.4" + "@babel/plugin-transform-property-literals" "^7.2.0" + "@babel/plugin-transform-regenerator" "^7.4.5" + "@babel/plugin-transform-reserved-words" "^7.2.0" + "@babel/plugin-transform-shorthand-properties" "^7.2.0" + "@babel/plugin-transform-spread" "^7.2.0" + "@babel/plugin-transform-sticky-regex" "^7.2.0" + "@babel/plugin-transform-template-literals" "^7.4.4" + "@babel/plugin-transform-typeof-symbol" "^7.2.0" + "@babel/plugin-transform-unicode-regex" "^7.4.4" + "@babel/types" "^7.6.0" + browserslist "^4.6.0" + core-js-compat "^3.1.1" + invariant "^2.2.2" + js-levenshtein "^1.1.3" + semver "^5.5.0" + +"@babel/preset-env@^7.4.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.5.tgz#bc470b53acaa48df4b8db24a570d6da1fef53c9a" + integrity sha512-GMZQka/+INwsMz1A5UEql8tG015h5j/qjptpKY2gJ7giy8ohzU710YciJB5rcKsWGWHiW3RUnHib0E5/m3Tp3A== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-async-generator-functions" "^7.2.0" + "@babel/plugin-proposal-dynamic-import" "^7.5.0" + "@babel/plugin-proposal-json-strings" "^7.2.0" + "@babel/plugin-proposal-object-rest-spread" "^7.5.5" + "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-syntax-async-generators" "^7.2.0" + "@babel/plugin-syntax-dynamic-import" "^7.2.0" + "@babel/plugin-syntax-json-strings" "^7.2.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" + "@babel/plugin-transform-arrow-functions" "^7.2.0" + "@babel/plugin-transform-async-to-generator" "^7.5.0" + "@babel/plugin-transform-block-scoped-functions" "^7.2.0" + "@babel/plugin-transform-block-scoping" "^7.5.5" + "@babel/plugin-transform-classes" "^7.5.5" + "@babel/plugin-transform-computed-properties" "^7.2.0" + "@babel/plugin-transform-destructuring" "^7.5.0" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/plugin-transform-duplicate-keys" "^7.5.0" + "@babel/plugin-transform-exponentiation-operator" "^7.2.0" + "@babel/plugin-transform-for-of" "^7.4.4" + "@babel/plugin-transform-function-name" "^7.4.4" + "@babel/plugin-transform-literals" "^7.2.0" + "@babel/plugin-transform-member-expression-literals" "^7.2.0" + "@babel/plugin-transform-modules-amd" "^7.5.0" + "@babel/plugin-transform-modules-commonjs" "^7.5.0" + "@babel/plugin-transform-modules-systemjs" "^7.5.0" + "@babel/plugin-transform-modules-umd" "^7.2.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.4.5" + "@babel/plugin-transform-new-target" "^7.4.4" + "@babel/plugin-transform-object-super" "^7.5.5" + "@babel/plugin-transform-parameters" "^7.4.4" + "@babel/plugin-transform-property-literals" "^7.2.0" + "@babel/plugin-transform-regenerator" "^7.4.5" + "@babel/plugin-transform-reserved-words" "^7.2.0" + "@babel/plugin-transform-shorthand-properties" "^7.2.0" + "@babel/plugin-transform-spread" "^7.2.0" + "@babel/plugin-transform-sticky-regex" "^7.2.0" + "@babel/plugin-transform-template-literals" "^7.4.4" + "@babel/plugin-transform-typeof-symbol" "^7.2.0" + "@babel/plugin-transform-unicode-regex" "^7.4.4" + "@babel/types" "^7.5.5" + browserslist "^4.6.0" + core-js-compat "^3.1.1" + invariant "^2.2.2" + js-levenshtein "^1.1.3" + semver "^5.5.0" + +"@babel/preset-react@7.0.0", "@babel/preset-react@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0" + integrity sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-react-display-name" "^7.0.0" + "@babel/plugin-transform-react-jsx" "^7.0.0" + "@babel/plugin-transform-react-jsx-self" "^7.0.0" + "@babel/plugin-transform-react-jsx-source" "^7.0.0" + +"@babel/preset-typescript@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.6.0.tgz#25768cb8830280baf47c45ab1a519a9977498c98" + integrity sha512-4xKw3tTcCm0qApyT6PqM9qniseCE79xGHiUnNdKGdxNsGUc2X7WwZybqIpnTmoukg3nhPceI5KPNzNqLNeIJww== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.6.0" + +"@babel/runtime@7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205" + integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ== + dependencies: + regenerator-runtime "^0.13.2" + +"@babel/runtime@^7.0.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" + integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== + dependencies: + regenerator-runtime "^0.13.2" + +"@babel/runtime@^7.1.2": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" + integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== + dependencies: + regenerator-runtime "^0.12.0" + +"@babel/runtime@^7.3.1": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" + integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ== + dependencies: + regenerator-runtime "^0.13.2" + +"@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3": + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" + integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw== + dependencies: + regenerator-runtime "^0.13.2" + +"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" + integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + +"@babel/template@^7.6.0", "@babel/template@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.0.tgz#4fadc1b8e734d97f56de39c77de76f2562e597d0" + integrity sha512-OKcwSYOW1mhWbnTBgQY5lvg1Fxg+VyfQGjcBduZFljfc044J5iDlnDSfhQ867O17XHiSCxYHUxHg2b7ryitbUQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.7.0" + "@babel/types" "^7.7.0" + +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb" + integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.5.5" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.5.5" + "@babel/types" "^7.5.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + +"@babel/traverse@^7.6.0", "@babel/traverse@^7.7.0": + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.7.2.tgz#ef0a65e07a2f3c550967366b3d9b62a2dcbeae09" + integrity sha512-TM01cXib2+rgIZrGJOLaHV/iZUAxf4A0dt5auY6KNZ+cm6aschuJGqKJM3ROTt3raPUdIDk9siAufIFEleRwtw== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.7.2" + "@babel/helper-function-name" "^7.7.0" + "@babel/helper-split-export-declaration" "^7.7.0" + "@babel/parser" "^7.7.2" + "@babel/types" "^7.7.2" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + +"@babel/types@7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.3.tgz#3f07d96f854f98e2fbd45c64b0cb942d11e8ba09" + integrity sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a" + integrity sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + +"@babel/types@^7.6.0", "@babel/types@^7.6.3", "@babel/types@^7.7.0", "@babel/types@^7.7.2": + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.7.2.tgz#550b82e5571dcd174af576e23f0adba7ffc683f7" + integrity sha512-YTf6PXoh3+eZgRCBzzP25Bugd2ngmpQVrk7kXX0i5N9BO7TFBtIgZYs7WtxtOGs8e6A4ZI7ECkbBCEHeXocvOA== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + +"@cnakazawa/watch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" + integrity sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + +"@csstools/convert-colors@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" + integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== + +"@csstools/normalize.css@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-9.0.1.tgz#c27b391d8457d1e893f1eddeaf5e5412d12ffbb5" + integrity sha512-6It2EVfGskxZCQhuykrfnALg7oVeiI6KclWSmGDqB0AiInVrTGB9Jp9i4/Ad21u9Jde/voVQz6eFX/eSg/UsPA== + +"@date-io/core@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.1.0.tgz#b7b8fbd9b5e57cc6b55a5af2d98394eafdf42921" + integrity sha512-PyjhyR2fbp7Q8xpB5zoOyT3dqr8Bn4kXfREf1w6AnQalwdftNxChB2/p88fI1qsx8KNmHDJY12eCgLoZaPegTw== + +"@date-io/moment@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-1.1.0.tgz#993c71b77f52f078de6a28e61ddfae757d39dc64" + integrity sha512-U7Vrhk7nlrKMtlLuNs7DAWmMsMBTgWFAvgpKg6Z5Opo/U7ab5DvI8nLks5dqJbeEFQDSlO+KCmJWfsL9hVI3Jw== + dependencies: + "@date-io/core" "^1.1.0" + +"@emotion/hash@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.1.tgz#9833722341379fb7d67f06a4b00ab3c37913da53" + integrity sha512-OYpa/Sg+2GDX+jibUfpZVn1YqSVRpYmTLF2eyAfrFTIJSbwyIrc+YscayoykvaOME/wV4BV0Sa0yqdMrgse6mA== + +"@endemolshinegroup/cosmiconfig-typescript-loader@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-1.0.1.tgz#484ee6f4e9209ffde5d3edbdacf03e0bc5ee0c67" + integrity sha512-bhUR9035PbgL6A/nfLayjoqKo4W7hCtzxqVxq2cgDB+Ndpsa3dGIr71/ymgY3vCTCQaufkFxAcEeoECyJ498CA== + dependencies: + lodash.get "^4" + make-error "^1" + ts-node "^8" + tslib "^1" + +"@fullcalendar/core@^4.3.1", "@fullcalendar/core@~4.3.0": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@fullcalendar/core/-/core-4.3.1.tgz#a061c6d2e998d4155439dbc8aefdfe01cdf648d8" + integrity sha512-Eh+p/wpMkWGu26f8NpfQK9ecQMoZxX/aopv+0+4/CH+Ip0paP6iEc40JYgTz7RFl0bFqV1dvwyGyUZ4+9ZeySA== + +"@fullcalendar/daygrid@^4.3.0", "@fullcalendar/daygrid@~4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@fullcalendar/daygrid/-/daygrid-4.3.0.tgz#1b4ab9fdc238dba77921cb7c5bd2d813c834e905" + integrity sha512-OrGaRIGbELESOXOCXtoQVY4cOxnxmN7OrjDGbMoVITdoXWuIZ6sFNO84WcBQoRaIbkmqVM0SAU2MENwy+eEwqQ== + +"@fullcalendar/interaction@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@fullcalendar/interaction/-/interaction-4.3.0.tgz#a52d22e4fa2666d5032c77afde75ecfad95c0f3b" + integrity sha512-kaKV+tdgH/oIrwTSMGYFec989L5r+KMYJ9ybwLc8G3LbMVcebo8fAlN9VizmGQAuopKfyvOw8yzJdjfmVCCRYQ== + +"@fullcalendar/moment@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@fullcalendar/moment/-/moment-4.3.0.tgz#7db57cc9a1d11055974c42388fab16bfa523d74c" + integrity sha512-Ek+wJL+PcLpBddrOoIFLGnqsp1LG5Jm6lWeqA37sKvTwx/02IgZsHzx+lrhzR6l1JTFmtAfuANQeYdYYjVor1Q== + +"@fullcalendar/react@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@fullcalendar/react/-/react-4.3.0.tgz#db00bb0486daacdf139923e71d909cd1a572c23a" + integrity sha512-nNSrStb0YLYPeHySrWlQrRrP7CX922tkAKJWkTtdCnIljYnRjdU0B75XMmt1VH1EpVPY1JPCkMBsWPZF2mcfag== + dependencies: + "@fullcalendar/core" "~4.3.0" + fast-deep-equal "^2.0.1" + +"@fullcalendar/timegrid@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@fullcalendar/timegrid/-/timegrid-4.3.0.tgz#bf003499319e5e66135bb7b8d8ed274b1a62ca91" + integrity sha512-9JCSZbzw2Hi+X893l5X9ViZaWjxh+sDy59o6nKx0gYKwSfs/vKpmk73GBQfKyUv8orkjpnTdPFfeLDZqlzBP4Q== + dependencies: + "@fullcalendar/daygrid" "~4.3.0" + +"@hapi/address@2.x.x": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.0.0.tgz#9f05469c88cb2fd3dcd624776b54ee95c312126a" + integrity sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw== + +"@hapi/hoek@6.x.x": + version "6.2.4" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-6.2.4.tgz#4b95fbaccbfba90185690890bdf1a2fbbda10595" + integrity sha512-HOJ20Kc93DkDVvjwHyHawPwPkX44sIrbXazAUDiUXaY2R9JwQGo2PhFfnQtdrsIe4igjG2fPgMra7NYw7qhy0A== + +"@hapi/hoek@8.x.x": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.2.0.tgz#079a133be240ff866ad8532eaa46a691bdd91117" + integrity sha512-pR2ZgiP562aiaQvQ98WgfqfTrm+xG+7hwHRPEiYZ+7U1OHAAb4OVZJIalCP03bMqYSioQzflzVTVrybSwDBn1Q== + +"@hapi/joi@^15.0.0": + version "15.1.0" + resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.0.tgz#940cb749b5c55c26ab3b34ce362e82b6162c8e7a" + integrity sha512-n6kaRQO8S+kepUTbXL9O/UOL788Odqs38/VOfoCrATDtTvyfiO3fgjlSRaNkHabpTLgM7qru9ifqXlXbXk8SeQ== + dependencies: + "@hapi/address" "2.x.x" + "@hapi/hoek" "6.x.x" + "@hapi/marker" "1.x.x" + "@hapi/topo" "3.x.x" + +"@hapi/marker@1.x.x": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@hapi/marker/-/marker-1.0.0.tgz#65b0b2b01d1be06304886ce9b4b77b1bfb21a769" + integrity sha512-JOfdekTXnJexfE8PyhZFyHvHjt81rBFSAbTIRAhF2vv/2Y1JzoKsGqxH/GpZJoF7aEfYok8JVcAHmSz1gkBieA== + +"@hapi/topo@3.x.x": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.3.tgz#c7a02e0d936596d29f184e6d7fdc07e8b5efce11" + integrity sha512-JmS9/vQK6dcUYn7wc2YZTqzIKubAQcJKu2KCKAru6es482U5RT5fP1EXCPtlXpiK7PR0On/kpQKI4fRKkzpZBQ== + dependencies: + "@hapi/hoek" "8.x.x" + +"@jest/console@^24.7.1": + version "24.7.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545" + integrity sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg== + dependencies: + "@jest/source-map" "^24.3.0" + chalk "^2.0.1" + slash "^2.0.0" + +"@jest/console@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" + integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ== + dependencies: + "@jest/source-map" "^24.9.0" + chalk "^2.0.1" + slash "^2.0.0" + +"@jest/core@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4" + integrity sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A== + dependencies: + "@jest/console" "^24.7.1" + "@jest/reporters" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-changed-files "^24.9.0" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-resolve-dependencies "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + jest-watcher "^24.9.0" + micromatch "^3.1.10" + p-each-series "^1.0.0" + realpath-native "^1.1.0" + rimraf "^2.5.4" + slash "^2.0.0" + strip-ansi "^5.0.0" + +"@jest/environment@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18" + integrity sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ== + dependencies: + "@jest/fake-timers" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + +"@jest/fake-timers@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.8.0.tgz#2e5b80a4f78f284bcb4bd5714b8e10dd36a8d3d1" + integrity sha512-2M4d5MufVXwi6VzZhJ9f5S/wU4ud2ck0kxPof1Iz3zWx6Y+V2eJrES9jEktB6O3o/oEyk+il/uNu9PvASjWXQw== + dependencies: + "@jest/types" "^24.8.0" + jest-message-util "^24.8.0" + jest-mock "^24.8.0" + +"@jest/fake-timers@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93" + integrity sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A== + dependencies: + "@jest/types" "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + +"@jest/reporters@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43" + integrity sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.2" + istanbul-lib-coverage "^2.0.2" + istanbul-lib-instrument "^3.0.1" + istanbul-lib-report "^2.0.4" + istanbul-lib-source-maps "^3.0.1" + istanbul-reports "^2.2.6" + jest-haste-map "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + node-notifier "^5.4.2" + slash "^2.0.0" + source-map "^0.6.0" + string-length "^2.0.0" + +"@jest/source-map@^24.3.0": + version "24.3.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.3.0.tgz#563be3aa4d224caf65ff77edc95cd1ca4da67f28" + integrity sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.1.15" + source-map "^0.6.0" + +"@jest/source-map@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" + integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.1.15" + source-map "^0.6.0" + +"@jest/test-result@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.8.0.tgz#7675d0aaf9d2484caa65e048d9b467d160f8e9d3" + integrity sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng== + dependencies: + "@jest/console" "^24.7.1" + "@jest/types" "^24.8.0" + "@types/istanbul-lib-coverage" "^2.0.0" + +"@jest/test-result@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca" + integrity sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA== + dependencies: + "@jest/console" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/istanbul-lib-coverage" "^2.0.0" + +"@jest/test-sequencer@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31" + integrity sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A== + dependencies: + "@jest/test-result" "^24.9.0" + jest-haste-map "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + +"@jest/transform@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.9.0.tgz#4ae2768b296553fadab09e9ec119543c90b16c56" + integrity sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^24.9.0" + babel-plugin-istanbul "^5.1.0" + chalk "^2.0.1" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.1.15" + jest-haste-map "^24.9.0" + jest-regex-util "^24.9.0" + jest-util "^24.9.0" + micromatch "^3.1.10" + pirates "^4.0.1" + realpath-native "^1.1.0" + slash "^2.0.0" + source-map "^0.6.1" + write-file-atomic "2.4.1" + +"@jest/types@^24.8.0": + version "24.8.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.8.0.tgz#f31e25948c58f0abd8c845ae26fcea1491dea7ad" + integrity sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^12.0.9" + +"@jest/types@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" + integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^13.0.0" + +"@material-ui/core@^4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.6.1.tgz#039f97443547a88c41d290deabfb4a044c6031ec" + integrity sha512-TljDMCJmi1zh7JhAFTp8qjIlbkVACiNftrcitzJJ+hAqpuP9PTO4euEkkAuYjISfUFZl3Z4kaOrBwN1HDrhIOQ== + dependencies: + "@babel/runtime" "^7.4.4" + "@material-ui/styles" "^4.6.0" + "@material-ui/system" "^4.5.2" + "@material-ui/types" "^4.1.1" + "@material-ui/utils" "^4.5.2" + "@types/react-transition-group" "^4.2.0" + clsx "^1.0.2" + convert-css-length "^2.0.1" + hoist-non-react-statics "^3.2.1" + normalize-scroll-left "^0.2.0" + popper.js "^1.14.1" + prop-types "^15.7.2" + react-transition-group "^4.3.0" + +"@material-ui/icons@^4.5.1": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.5.1.tgz#6963bad139e938702ece85ca43067688018f04f8" + integrity sha512-YZ/BgJbXX4a0gOuKWb30mBaHaoXRqPanlePam83JQPZ/y4kl+3aW0Wv9tlR70hB5EGAkEJGW5m4ktJwMgxQAeA== + dependencies: + "@babel/runtime" "^7.4.4" + +"@material-ui/pickers@^3.2.8": + version "3.2.8" + resolved "https://registry.yarnpkg.com/@material-ui/pickers/-/pickers-3.2.8.tgz#bef66fdd9f8c388c8bb0f2841cc8be1162b0d501" + integrity sha512-uWGIUpfxPXZItCWnnF3ZSWgjv40wnlhceG6QhSQ1ARpqxU/4TEcnflR7FO54ULKOsyjwF+sgWPrqV/rPgGtPXA== + dependencies: + "@babel/runtime" "^7.6.0" + "@types/styled-jsx" "^2.2.8" + clsx "^1.0.2" + react-transition-group "^4.0.0" + rifm "^0.7.0" + +"@material-ui/styles@^4.6.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.6.0.tgz#15679fab6dcbe0cc2416f01a22966f3ea26607c5" + integrity sha512-lqqh4UEMdIYcU1Yth4pQyMTah02uAkg3NOT3MirN9FUexdL8pNA6zCHigEgDSfwmvnXyxHhxTkphfy0DRfnt9w== + dependencies: + "@babel/runtime" "^7.4.4" + "@emotion/hash" "^0.7.1" + "@material-ui/types" "^4.1.1" + "@material-ui/utils" "^4.5.2" + clsx "^1.0.2" + csstype "^2.5.2" + hoist-non-react-statics "^3.2.1" + jss "^10.0.0" + jss-plugin-camel-case "^10.0.0" + jss-plugin-default-unit "^10.0.0" + jss-plugin-global "^10.0.0" + jss-plugin-nested "^10.0.0" + jss-plugin-props-sort "^10.0.0" + jss-plugin-rule-value-function "^10.0.0" + jss-plugin-vendor-prefixer "^10.0.0" + prop-types "^15.7.2" + +"@material-ui/system@^4.5.2": + version "4.5.2" + resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.5.2.tgz#7143bd8422a3f33f435c23f378136254004bbd60" + integrity sha512-h9RWvdM9XKlHHqwiuhyvWdobptQkHli+m2jJFs7i1AI/hmGsIc4reDmS7fInhETgt/Txx7uiAIznfRNIIVHmQw== + dependencies: + "@babel/runtime" "^7.4.4" + "@material-ui/utils" "^4.5.2" + prop-types "^15.7.2" + +"@material-ui/types@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-4.1.1.tgz#b65e002d926089970a3271213a3ad7a21b17f02b" + integrity sha512-AN+GZNXytX9yxGi0JOfxHrRTbhFybjUJ05rnsBVjcB+16e466Z0Xe5IxawuOayVZgTBNDxmPKo5j4V6OnMtaSQ== + dependencies: + "@types/react" "*" + +"@material-ui/utils@^4.5.2": + version "4.5.2" + resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.5.2.tgz#4c2fb531d357cf0da8cece53b588dff9b0bde934" + integrity sha512-zhbNfHd1gLa8At6RPDG7uMZubHxbY+LtM6IkSfeWi6Lo4Ax80l62YaN1QmUpO1IvGCkn/j62tQX3yObiQZrJsQ== + dependencies: + "@babel/runtime" "^7.4.4" + prop-types "^15.7.2" + react-is "^16.8.6" + +"@mrmlnc/readdir-enhanced@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" + integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== + dependencies: + call-me-maybe "^1.0.1" + glob-to-regexp "^0.3.0" + +"@nodelib/fs.stat@^1.1.2": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" + integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== + +"@oclif/color@^0.0.0": + version "0.0.0" + resolved "https://registry.yarnpkg.com/@oclif/color/-/color-0.0.0.tgz#54939bbd16d1387511bf1a48ccda1a417248e6a9" + integrity sha512-KKd3W7eNwfNF061tr663oUNdt8EMnfuyf5Xv55SGWA1a0rjhWqS/32P7OeB7CbXcJUBdfVrPyR//1afaW12AWw== + dependencies: + ansi-styles "^3.2.1" + supports-color "^5.4.0" + tslib "^1" + +"@oclif/command@1.5.19", "@oclif/command@^1.4.31", "@oclif/command@^1.5.1", "@oclif/command@^1.5.10", "@oclif/command@^1.5.12", "@oclif/command@^1.5.13", "@oclif/command@^1.5.3": + version "1.5.19" + resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.5.19.tgz#13f472450eb83bd6c6871a164c03eadb5e1a07ed" + integrity sha512-6+iaCMh/JXJaB2QWikqvGE9//wLEVYYwZd5sud8aLoLKog1Q75naZh2vlGVtg5Mq/NqpqGQvdIjJb3Bm+64AUQ== + dependencies: + "@oclif/config" "^1" + "@oclif/errors" "^1.2.2" + "@oclif/parser" "^3.8.3" + "@oclif/plugin-help" "^2" + debug "^4.1.1" + semver "^5.6.0" + +"@oclif/config@1.13.3", "@oclif/config@^1", "@oclif/config@^1.12.8", "@oclif/config@^1.6.22": + version "1.13.3" + resolved "https://registry.yarnpkg.com/@oclif/config/-/config-1.13.3.tgz#1b13e18d0e4242ddbd9cbd100f0eec819aa2bf8c" + integrity sha512-qs5XvGRw+1M41abOKCjd0uoeHCgsMxa2MurD2g2K8CtQlzlMXl0rW5idVeimIg5208LLuxkfzQo8TKAhhRCWLg== + dependencies: + "@oclif/parser" "^3.8.0" + debug "^4.1.1" + tslib "^1.9.3" + +"@oclif/errors@1.2.2", "@oclif/errors@^1.2.1", "@oclif/errors@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.2.2.tgz#9d8f269b15f13d70aa93316fed7bebc24688edc2" + integrity sha512-Eq8BFuJUQcbAPVofDxwdE0bL14inIiwt5EaKRVY9ZDIG11jwdXZqiQEECJx0VfnLyUZdYfRd/znDI/MytdJoKg== + dependencies: + clean-stack "^1.3.0" + fs-extra "^7.0.0" + indent-string "^3.2.0" + strip-ansi "^5.0.0" + wrap-ansi "^4.0.0" + +"@oclif/linewrap@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@oclif/linewrap/-/linewrap-1.0.0.tgz#aedcb64b479d4db7be24196384897b5000901d91" + integrity sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw== + +"@oclif/parser@^3.8.0", "@oclif/parser@^3.8.3": + version "3.8.4" + resolved "https://registry.yarnpkg.com/@oclif/parser/-/parser-3.8.4.tgz#1a90fc770a42792e574fb896325618aebbe8c9e4" + integrity sha512-cyP1at3l42kQHZtqDS3KfTeyMvxITGwXwH1qk9ktBYvqgMp5h4vHT+cOD74ld3RqJUOZY/+Zi9lb4Tbza3BtuA== + dependencies: + "@oclif/linewrap" "^1.0.0" + chalk "^2.4.2" + tslib "^1.9.3" + +"@oclif/plugin-autocomplete@0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@oclif/plugin-autocomplete/-/plugin-autocomplete-0.1.4.tgz#ae993f196ca0085a63e3141814eaf9dc6c178418" + integrity sha512-ZyxJyL6jSt9Df68Smeu14xhZZwELE9IB5twhie1/56rt62nG6TJB4CZhaMqRk+33MDfU3JyWxNbIDMNMESlGqg== + dependencies: + "@oclif/command" "^1.4.31" + "@oclif/config" "^1.6.22" + "@types/fs-extra" "^5.0.2" + chalk "^2.4.1" + cli-ux "^4.4.0" + debug "^3.1.0" + fs-extra "^6.0.1" + moment "^2.22.1" + +"@oclif/plugin-help@2.2.1", "@oclif/plugin-help@^2": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-2.2.1.tgz#cb4d23e453b19340b07c48fb299948b7c74366d3" + integrity sha512-psEA3t41MSGBErLk6xCaAq2jKrRtx3Br+kHpd43vZeGEeZ7Gos4wgK0JAaHBbvhvUQskCHg8dzoqv4XEeTWeVQ== + dependencies: + "@oclif/command" "^1.5.13" + chalk "^2.4.1" + indent-string "^3.2.0" + lodash.template "^4.4.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + widest-line "^2.0.1" + wrap-ansi "^4.0.0" + +"@oclif/plugin-not-found@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@oclif/plugin-not-found/-/plugin-not-found-1.2.3.tgz#00f9e7a0a793a5e2f616d8280075f4592de8f079" + integrity sha512-Igbw2T4gLrb/f28Llr730FeMXBSI2PXdky2YvQfsZeQGDsyBZmC4gprJJtmrMWQcjz0B51IInRBnZYERvwfIpw== + dependencies: + "@oclif/color" "^0.0.0" + "@oclif/command" "^1.5.3" + cli-ux "^4.9.0" + fast-levenshtein "^2.0.6" + lodash "^4.17.13" + +"@oclif/plugin-plugins@1.7.8": + version "1.7.8" + resolved "https://registry.yarnpkg.com/@oclif/plugin-plugins/-/plugin-plugins-1.7.8.tgz#51e07cb85bf9cd36a6de8f3e441b7d705a5e7105" + integrity sha512-GxLxaf8Lk1RqHVAIBZyA7hmhU7u5oV97i/OsWgFPdjPaT+BmWlWXR8IpmtA8giNo6atR+JpfgDmYndMU75zYUQ== + dependencies: + "@oclif/color" "^0.0.0" + "@oclif/command" "^1.5.12" + chalk "^2.4.2" + cli-ux "^5.2.1" + debug "^4.1.0" + fs-extra "^7.0.1" + http-call "^5.2.2" + load-json-file "^5.2.0" + npm-run-path "^3.0.0" + semver "^5.6.0" + tslib "^1.9.3" + yarn "^1.15.0" + +"@oclif/plugin-warn-if-update-available@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@oclif/plugin-warn-if-update-available/-/plugin-warn-if-update-available-1.7.0.tgz#5a72abe39ce0b831eb4ae81cb64eb4b9f3ea424a" + integrity sha512-Nwyz3BJ8RhsfQ+OmFSsJSPIfn5YJqMrCzPh72Zgo2jqIjKIBWD8N9vTTe4kZlpeUUn77SyXFfwlBQbNCL5OEuQ== + dependencies: + "@oclif/command" "^1.5.10" + "@oclif/config" "^1.12.8" + "@oclif/errors" "^1.2.2" + chalk "^2.4.1" + debug "^4.1.0" + fs-extra "^7.0.0" + http-call "^5.2.2" + lodash.template "^4.4.0" + semver "^5.6.0" + +"@oclif/screen@^1.0.3": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493" + integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw== + +"@rooks/use-interval@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@rooks/use-interval/-/use-interval-1.2.0.tgz#7c7c0b082a016eff39b59904aa2ddb2c7491afad" + integrity sha512-gtYrJUNdYgflZRGwT7NjVqLRiD3EVT1ZxDqDCTStMN5jWtgIZulm8OJ3ZzZn62UTXtoSPcE/pJkQDccaD/NzhA== + +"@rooks/use-timeout@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@rooks/use-timeout/-/use-timeout-3.0.1.tgz#4a649d2e9a8b90cb1c5f364372c2f10dcb3c6f77" + integrity sha512-pJbNn7jjMNQpyPCOnBA2VFkwTMW3rCSXRDX9v+LJiEdq0o6HcaXO2bIHVBUs85n8B3yckpsfnIa7bVxbtWb6dA== + +"@samverschueren/stream-to-observable@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" + integrity sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg== + dependencies: + any-observable "^0.3.0" + +"@svgr/babel-plugin-add-jsx-attribute@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz#dadcb6218503532d6884b210e7f3c502caaa44b1" + integrity sha512-j7KnilGyZzYr/jhcrSYS3FGWMZVaqyCG0vzMCwzvei0coIkczuYMcniK07nI0aHJINciujjH11T72ICW5eL5Ig== + +"@svgr/babel-plugin-remove-jsx-attribute@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-4.2.0.tgz#297550b9a8c0c7337bea12bdfc8a80bb66f85abc" + integrity sha512-3XHLtJ+HbRCH4n28S7y/yZoEQnRpl0tvTZQsHqvaeNXPra+6vE5tbRliH3ox1yZYPCxrlqaJT/Mg+75GpDKlvQ== + +"@svgr/babel-plugin-remove-jsx-empty-expression@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-4.2.0.tgz#c196302f3e68eab6a05e98af9ca8570bc13131c7" + integrity sha512-yTr2iLdf6oEuUE9MsRdvt0NmdpMBAkgK8Bjhl6epb+eQWk6abBaX3d65UZ3E3FWaOwePyUgNyNCMVG61gGCQ7w== + +"@svgr/babel-plugin-replace-jsx-attribute-value@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-4.2.0.tgz#310ec0775de808a6a2e4fd4268c245fd734c1165" + integrity sha512-U9m870Kqm0ko8beHawRXLGLvSi/ZMrl89gJ5BNcT452fAjtF2p4uRzXkdzvGJJJYBgx7BmqlDjBN/eCp5AAX2w== + +"@svgr/babel-plugin-svg-dynamic-title@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-4.3.1.tgz#646c2f5b5770c2fe318d6e51492344c3d62ddb63" + integrity sha512-p6z6JJroP989jHWcuraeWpzdejehTmLUpyC9smhTBWyPN0VVGe2phbYxpPTV7Vh8XzmFrcG55idrnfWn/2oQEw== + +"@svgr/babel-plugin-svg-em-dimensions@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-4.2.0.tgz#9a94791c9a288108d20a9d2cc64cac820f141391" + integrity sha512-C0Uy+BHolCHGOZ8Dnr1zXy/KgpBOkEUYY9kI/HseHVPeMbluaX3CijJr7D4C5uR8zrc1T64nnq/k63ydQuGt4w== + +"@svgr/babel-plugin-transform-react-native-svg@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-4.2.0.tgz#151487322843359a1ca86b21a3815fd21a88b717" + integrity sha512-7YvynOpZDpCOUoIVlaaOUU87J4Z6RdD6spYN4eUb5tfPoKGSF9OG2NuhgYnq4jSkAxcpMaXWPf1cePkzmqTPNw== + +"@svgr/babel-plugin-transform-svg-component@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-4.2.0.tgz#5f1e2f886b2c85c67e76da42f0f6be1b1767b697" + integrity sha512-hYfYuZhQPCBVotABsXKSCfel2slf/yvJY8heTVX1PCTaq/IgASq1IyxPPKJ0chWREEKewIU/JMSsIGBtK1KKxw== + +"@svgr/babel-preset@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-4.3.1.tgz#62ffcb85d756580e8ce608e9d2ac3b9063be9e28" + integrity sha512-rPFKLmyhlh6oeBv3j2vEAj2nd2QbWqpoJLKzBLjwQVt+d9aeXajVaPNEqrES2spjXKR4OxfgSs7U0NtmAEkr0Q== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "^4.2.0" + "@svgr/babel-plugin-remove-jsx-attribute" "^4.2.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "^4.2.0" + "@svgr/babel-plugin-replace-jsx-attribute-value" "^4.2.0" + "@svgr/babel-plugin-svg-dynamic-title" "^4.3.1" + "@svgr/babel-plugin-svg-em-dimensions" "^4.2.0" + "@svgr/babel-plugin-transform-react-native-svg" "^4.2.0" + "@svgr/babel-plugin-transform-svg-component" "^4.2.0" + +"@svgr/core@^4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-4.3.2.tgz#939c89be670ad79b762f4c063f213f0e02535f2e" + integrity sha512-N+tP5CLFd1hP9RpO83QJPZY3NL8AtrdqNbuhRgBkjE/49RnMrrRsFm1wY8pueUfAGvzn6tSXUq29o6ah8RuR5w== + dependencies: + "@svgr/plugin-jsx" "^4.3.2" + camelcase "^5.3.1" + cosmiconfig "^5.2.1" + +"@svgr/hast-util-to-babel-ast@^4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-4.3.2.tgz#1d5a082f7b929ef8f1f578950238f630e14532b8" + integrity sha512-JioXclZGhFIDL3ddn4Kiq8qEqYM2PyDKV0aYno8+IXTLuYt6TOgHUbUAAFvqtb0Xn37NwP0BTHglejFoYr8RZg== + dependencies: + "@babel/types" "^7.4.4" + +"@svgr/plugin-jsx@^4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-4.3.2.tgz#ce9ddafc8cdd74da884c9f7af014afcf37f93d3c" + integrity sha512-+1GW32RvmNmCsOkMoclA/TppNjHPLMnNZG3/Ecscxawp051XJ2MkO09Hn11VcotdC2EPrDfT8pELGRo+kbZ1Eg== + dependencies: + "@babel/core" "^7.4.5" + "@svgr/babel-preset" "^4.3.1" + "@svgr/hast-util-to-babel-ast" "^4.3.2" + svg-parser "^2.0.0" + +"@svgr/plugin-svgo@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-4.3.1.tgz#daac0a3d872e3f55935c6588dd370336865e9e32" + integrity sha512-PrMtEDUWjX3Ea65JsVCwTIXuSqa3CG9px+DluF1/eo9mlDrgrtFE7NE/DjdhjJgSM9wenlVBzkzneSIUgfUI/w== + dependencies: + cosmiconfig "^5.2.1" + merge-deep "^3.0.2" + svgo "^1.2.2" + +"@svgr/webpack@4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-4.3.2.tgz#319d4471c8f3d5c3af35059274834d9b5b8fb956" + integrity sha512-F3VE5OvyOWBEd2bF7BdtFRyI6E9it3mN7teDw0JQTlVtc4HZEYiiLSl+Uf9Uub6IYHVGc+qIrxxDyeedkQru2w== + dependencies: + "@babel/core" "^7.4.5" + "@babel/plugin-transform-react-constant-elements" "^7.0.0" + "@babel/preset-env" "^7.4.5" + "@babel/preset-react" "^7.0.0" + "@svgr/core" "^4.3.2" + "@svgr/plugin-jsx" "^4.3.2" + "@svgr/plugin-svgo" "^4.3.1" + loader-utils "^1.2.3" + +"@types/babel__core@^7.1.0": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f" + integrity sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.0.2.tgz#d2112a6b21fad600d7674274293c85dce0cb47fc" + integrity sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307" + integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.7.tgz#2496e9ff56196cc1429c72034e07eab6121b6f3f" + integrity sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw== + dependencies: + "@babel/types" "^7.3.0" + +"@types/d3-path@*": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.8.tgz#48e6945a8ff43ee0a1ce85c8cfa2337de85c7c79" + integrity sha512-AZGHWslq/oApTAHu9+yH/Bnk63y9oFOMROtqPAtxl5uB6qm1x2lueWdVEjsjjV3Qc2+QfuzKIwIR5MvVBakfzA== + +"@types/d3-shape@*": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.2.tgz#a41d9d6b10d02e221696b240caf0b5d0f5a588ec" + integrity sha512-LtD8EaNYCaBRzHzaAiIPrfcL3DdIysc81dkGlQvv7WQP3+YXV7b0JJTtR1U3bzeRieS603KF4wUo+ZkJVenh8w== + dependencies: + "@types/d3-path" "*" + +"@types/eslint-visitor-keys@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" + integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== + +"@types/fs-extra@^5.0.2": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.1.0.tgz#2a325ef97901504a3828718c390d34b8426a10a1" + integrity sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ== + dependencies: + "@types/node" "*" + +"@types/graphql@^14.0.7": + version "14.5.0" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.5.0.tgz#a545fb3bc8013a3547cf2f07f5e13a33642b75d6" + integrity sha512-MOkzsEp1Jk5bXuAsHsUi6BVv0zCO+7/2PTiZMXWDSsMXvNU6w/PLMQT2vHn8hy2i0JqojPz1Sz6rsFjHtsU0lA== + dependencies: + graphql "*" + +"@types/history@*": + version "4.7.3" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.3.tgz#856c99cdc1551d22c22b18b5402719affec9839a" + integrity sha512-cS5owqtwzLN5kY+l+KgKdRJ/Cee8tlmQoGQuIE9tWnSmS3JMKzmxo2HIAk2wODMifGwO20d62xZQLYz+RLfXmw== + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + +"@types/istanbul-lib-report@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#e5471e7fa33c61358dd38426189c037a58433b8c" + integrity sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + +"@types/jest-diff@*": + version "20.0.1" + resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" + integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA== + +"@types/jest@^24.0.15": + version "24.0.15" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.15.tgz#6c42d5af7fe3b44ffff7cc65de7bf741e8fa427f" + integrity sha512-MU1HIvWUme74stAoc3mgAi+aMlgKOudgEvQDIm1v4RkrDudBh1T+NFp5sftpBAdXdx1J0PbdpJ+M2EsSOi1djA== + dependencies: + "@types/jest-diff" "*" + +"@types/json-schema@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" + integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== + +"@types/lodash.clonedeep@^4.5.6": + version "4.5.6" + resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.6.tgz#3b6c40a0affe0799a2ce823b440a6cf33571d32b" + integrity sha512-cE1jYr2dEg1wBImvXlNtp0xDoS79rfEdGozQVgliDZj1uERH4k+rmEMTudP9b4VQ8O6nRb5gPqft0QzEQGMQgA== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.134" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.134.tgz#9032b440122db3a2a56200e91191996161dde5b9" + integrity sha512-2/O0khFUCFeDlbi7sZ7ZFRCcT812fAeOLm7Ev4KbwASkZ575TDrDcY7YyaoHdTOzKcNbfiwLYZqPmoC4wadrsw== + +"@types/moment-timezone@^0.5.12": + version "0.5.12" + resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.12.tgz#0fb680c03db194fe8ff4551eaeb1eec8d3d80e9f" + integrity sha512-hnHH2+Efg2vExr/dSz+IX860nSiyk9Sk4pJF2EmS11lRpMcNXeB4KBW5xcgw2QPsb9amTXdsVNEe5IoJXiT0uw== + dependencies: + moment ">=2.14.0" + +"@types/node@*", "@types/node@>=6": + version "12.12.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.8.tgz#dab418655af39ce2fa99286a0bed21ef8072ac9d" + integrity sha512-XLla8N+iyfjvsa0KKV+BP/iGSoTmwxsu5Ci5sM33z9TjohF72DEz95iNvD6pPmemvbQgxAv/909G73gUn8QR7w== + +"@types/node@11.11.3": + version "11.11.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.3.tgz#7c6b0f8eaf16ae530795de2ad1b85d34bf2f5c58" + integrity sha512-wp6IOGu1lxsfnrD+5mX6qwSwWuqsdkKKxTN4aQc4wByHAKZJf9/D4KXPQ1POUjEbnCP5LMggB0OEFNY9OTsMqg== + +"@types/pretty-ms@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/pretty-ms/-/pretty-ms-4.0.0.tgz#5e5177b9e447fbc8446e6fd2b09aea045971f11d" + integrity sha512-l5aIszYIm3s4jIL4wLDwajEml1w3E4zqGDDUgvnroX1ux6YAxsCrgRHLUafgrHFGTHlx7TpwiwSwcHV8IBvccw== + +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + +"@types/q@^1.5.1": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" + integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== + +"@types/react-color@^2.17.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-2.17.0.tgz#7f3c958bb43ebeedc7e04309576a235d5233ce9d" + integrity sha512-NQCLW437DXzaV/XvtoH3cBW75f0KQ9ZtFvvXnn7QEudLTR5zGxLdsEhPffrateSizsG2CTml4X+2/2TyEisotQ== + dependencies: + "@types/react" "*" + +"@types/react-dom@^16.9.4": + version "16.9.4" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.4.tgz#0b58df09a60961dcb77f62d4f1832427513420df" + integrity sha512-fya9xteU/n90tda0s+FtN5Ym4tbgxpq/hb/Af24dvs6uYnYn+fspaxw5USlw0R8apDNwxsqumdRoCoKitckQqw== + dependencies: + "@types/react" "*" + +"@types/react-grid-layout@^0.16.7": + version "0.16.7" + resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-0.16.7.tgz#53d5f5034deb0c60e25a0fa578141e9a0982011f" + integrity sha512-A3tW9xySd03KGONkp8gP4+QRLuT1Mcx++m0hO0nZIM4H/Qwz8GsiDv+9okbmHk5HcsHwY5Jdsn6Cv50hwoNG+A== + dependencies: + "@types/react" "*" + +"@types/react-infinite@^0.0.34": + version "0.0.34" + resolved "https://registry.yarnpkg.com/@types/react-infinite/-/react-infinite-0.0.34.tgz#0b514f65c4ba80ad22dea079075e04345ddcaae2" + integrity sha512-QymnFjmUUjeqhT8ztQpK5dmNgCGv+FSB5IsImj5OwXpz58xIrnucIJZtF3ZqjyfmCEMBGUpnQZHh5Mkc2Yhj8w== + dependencies: + "@types/react" "*" + +"@types/react-router-dom@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.2.tgz#853f229f1f297513c0be84f7c914a08b778cfdf5" + integrity sha512-kRx8hoBflE4Dp7uus+j/0uMHR5uGTAvQtc4A3vOTWKS+epe0leCuxEx7HNT7XGUd1lH53/moWM51MV2YUyhzAg== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.4.5.tgz#1166997dc7eef2917b5ebce890ebecb32ee5c1b3" + integrity sha512-12+VOu1+xiC8RPc9yrgHCyLI79VswjtuqeS2gPrMcywH6tkc8rGIUhs4LaL3AJPqo5d+RPnfRpNKiJ7MK2Qhcg== + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react-router@^5.0.1": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.3.tgz#7c7ca717399af64d8733d8cb338dd43641b96f2d" + integrity sha512-0gGhmerBqN8CzlnDmSgGNun3tuZFXerUclWkqEhozdLaJtfcJRUTGkKaEKk+/MpHd1KDS1+o2zb/3PkBUiv2qQ== + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react-transition-group@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.2.3.tgz#4924133f7268694058e415bf7aea2d4c21131470" + integrity sha512-Hk8jiuT7iLOHrcjKP/ZVSyCNXK73wJAUz60xm0mVhiRujrdiI++j4duLiL282VGxwAgxetHQFfqA29LgEeSkFA== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "16.9.11" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.11.tgz#70e0b7ad79058a7842f25ccf2999807076ada120" + integrity sha512-UBT4GZ3PokTXSWmdgC/GeCGEJXE5ofWyibCcecRLUVN2ZBpXQGVgQGtG2foS7CrTKFKlQVVswLvf7Js6XA/CVQ== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + +"@types/react@16.8.8": + version "16.8.8" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.8.tgz#4b60a469fd2469f7aa6eaa0f8cfbc51f6d76e662" + integrity sha512-xwEvyet96u7WnB96kqY0yY7qxx/pEpU51QeACkKFtrgjjXITQn0oO1iwPEraXVgh10ZFPix7gs1R4OJXF7P5sg== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + +"@types/recharts-scale@*": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/recharts-scale/-/recharts-scale-1.0.0.tgz#348c9220d6d9062c44a9d585d686644a97f7e25d" + integrity sha512-HR/PrCcxYb2YHviTqH7CMdL1TUhUZLTUKzfrkMhxm1HTa5mg/QtP8XMiuSPz6dZ6wecazAOu8aYZ5DqkNlgHHQ== + +"@types/recharts@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@types/recharts/-/recharts-1.8.3.tgz#c66618dd9d170bd22b2eff23f09d65883728ae4a" + integrity sha512-Rv6O2tfS5VLhOO6zvSkzPv6o6J4wq81QLnj/lc2MbUhQMISb4046rvf2RSqqMNWi2+qG32AnND7JlsLipMDn+A== + dependencies: + "@types/d3-shape" "*" + "@types/react" "*" + "@types/recharts-scale" "*" + +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + +"@types/styled-jsx@^2.2.8": + version "2.2.8" + resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234" + integrity sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg== + dependencies: + "@types/react" "*" + +"@types/yargs-parser@*": + version "13.1.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228" + integrity sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg== + +"@types/yargs@^12.0.9": + version "12.0.12" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916" + integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw== + +"@types/yargs@^13.0.0": + version "13.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.3.tgz#76482af3981d4412d65371a318f992d33464a380" + integrity sha512-K8/LfZq2duW33XW/tFwEAfnZlqIfVsoyRB3kfXdPXYhl0nfM8mmh7GS0jg7WrX2Dgq/0Ha/pR1PaR+BvmWwjiQ== + dependencies: + "@types/yargs-parser" "*" + +"@types/zen-observable@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" + integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== + +"@typescript-eslint/eslint-plugin@^2.2.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.8.0.tgz#eca584d46094ebebc3cb3e9fb625bfbc904a534d" + integrity sha512-ohqul5s6XEB0AzPWZCuJF5Fd6qC0b4+l5BGEnrlpmvXxvyymb8yw8Bs4YMF8usNAeuCJK87eFIHy8g8GFvOtGA== + dependencies: + "@typescript-eslint/experimental-utils" "2.8.0" + eslint-utils "^1.4.3" + functional-red-black-tree "^1.0.1" + regexpp "^3.0.0" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz#208b4164d175587e9b03ce6fea97d55f19c30ca9" + integrity sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.8.0" + eslint-scope "^5.0.0" + +"@typescript-eslint/parser@^2.2.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.8.0.tgz#e10f7c40c8cf2fb19920c879311e6c46ad17bacb" + integrity sha512-NseXWzhkucq+JM2HgqAAoKEzGQMb5LuTRjFPLQzGIdLthXMNUfuiskbl7QSykvWW6mvzCtYbw1fYWGa2EIaekw== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.8.0" + "@typescript-eslint/typescript-estree" "2.8.0" + eslint-visitor-keys "^1.1.0" + +"@typescript-eslint/typescript-estree@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz#fcc3fe6532840085d29b75432c8a59895876aeca" + integrity sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + tsutils "^3.17.1" + +"@webassemblyjs/ast@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" + integrity sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ== + dependencies: + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" + +"@webassemblyjs/floating-point-hex-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721" + integrity sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ== + +"@webassemblyjs/helper-api-error@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7" + integrity sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA== + +"@webassemblyjs/helper-buffer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204" + integrity sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q== + +"@webassemblyjs/helper-code-frame@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e" + integrity sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ== + dependencies: + "@webassemblyjs/wast-printer" "1.8.5" + +"@webassemblyjs/helper-fsm@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452" + integrity sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow== + +"@webassemblyjs/helper-module-context@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245" + integrity sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g== + dependencies: + "@webassemblyjs/ast" "1.8.5" + mamacro "^0.0.3" + +"@webassemblyjs/helper-wasm-bytecode@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61" + integrity sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ== + +"@webassemblyjs/helper-wasm-section@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf" + integrity sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + +"@webassemblyjs/ieee754@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e" + integrity sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10" + integrity sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc" + integrity sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw== + +"@webassemblyjs/wasm-edit@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a" + integrity sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/helper-wasm-section" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-opt" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + "@webassemblyjs/wast-printer" "1.8.5" + +"@webassemblyjs/wasm-gen@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc" + integrity sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" + +"@webassemblyjs/wasm-opt@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264" + integrity sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + +"@webassemblyjs/wasm-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d" + integrity sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" + +"@webassemblyjs/wast-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c" + integrity sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/floating-point-hex-parser" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-code-frame" "1.8.5" + "@webassemblyjs/helper-fsm" "1.8.5" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/wast-printer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc" + integrity sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" + "@xtuc/long" "4.2.2" + +"@wry/context@^0.4.0": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.4.4.tgz#e50f5fa1d6cfaabf2977d1fda5ae91717f8815f8" + integrity sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag== + dependencies: + "@types/node" ">=6" + tslib "^1.9.3" + +"@wry/equality@^0.1.2", "@wry/equality@^0.1.9": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909" + integrity sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ== + dependencies: + tslib "^1.9.3" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abab@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" + integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +acorn-globals@^4.1.0, acorn-globals@^4.3.0: + version "4.3.3" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.3.tgz#a86f75b69680b8780d30edd21eee4e0ea170c05e" + integrity sha512-vkR40VwS2SYO98AIeFvzWWh+xyc2qi9s7OoXSFEGIP/rOJKzjnhykaZJNnHdoq4BL2gGxI5EZOU16z896EYnOQ== + dependencies: + acorn "^6.0.1" + acorn-walk "^6.0.1" + +acorn-jsx@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e" + integrity sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg== + +acorn-walk@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" + integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== + +acorn@^5.5.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" + integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== + +acorn@^6.0.1, acorn@^6.0.4, acorn@^6.0.7, acorn@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.1.tgz#3ed8422d6dec09e6121cc7a843ca86a330a86b51" + integrity sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q== + +address@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" + integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== + +address@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/address/-/address-1.1.0.tgz#ef8e047847fcd2c5b6f50c16965f924fd99fe709" + integrity sha512-4diPfzWbLEIElVG4AnqP+00SULlPzNuyJFNnmMrLgyaxG6tZXJ1sn7mjBu4fHrJE+Yp/jgylOweJn2xsLMFggQ== + +adjust-sourcemap-loader@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-2.0.0.tgz#6471143af75ec02334b219f54bc7970c52fb29a4" + integrity sha512-4hFsTsn58+YjrU9qKzML2JSSDqKvN8mUGQ0nNIrfPi8hmIONT4L3uUaT6MKdMsZ9AjsU6D2xDkZxCkbQPxChrA== + dependencies: + assert "1.4.1" + camelcase "5.0.0" + loader-utils "1.2.3" + object-path "0.11.4" + regex-parser "2.2.10" + +airbnb-prop-types@^2.13.2: + version "2.15.0" + resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz#5287820043af1eb469f5b0af0d6f70da6c52aaef" + integrity sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA== + dependencies: + array.prototype.find "^2.1.0" + function.prototype.name "^1.1.1" + has "^1.0.3" + is-regex "^1.0.4" + object-is "^1.0.1" + object.assign "^4.1.0" + object.entries "^1.1.0" + prop-types "^15.7.2" + prop-types-exact "^1.2.0" + react-is "^16.9.0" + +ajv-errors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" + integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== + +ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" + integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== + +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: + version "6.10.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +alphanum-sort@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= + +ansi-colors@^3.0.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + +ansi-escapes@^3.0.0, ansi-escapes@^3.1.0, ansi-escapes@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-escapes@^4.2.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d" + integrity sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg== + dependencies: + type-fest "^0.8.1" + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.0.0, ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansicolors@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= + +any-observable@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" + integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +apollo-boost@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.4.4.tgz#7c278dac6cb6fa3f2f710c56baddc6e3ae730651" + integrity sha512-ASngBvazmp9xNxXfJ2InAzfDwz65o4lswlEPrWoN35scXmCz8Nz4k3CboUXbrcN/G0IExkRf/W7o9Rg0cjEBqg== + dependencies: + apollo-cache "^1.3.2" + apollo-cache-inmemory "^1.6.3" + apollo-client "^2.6.4" + apollo-link "^1.0.6" + apollo-link-error "^1.0.3" + apollo-link-http "^1.3.1" + graphql-tag "^2.4.2" + ts-invariant "^0.4.0" + tslib "^1.9.3" + +apollo-cache-inmemory@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d" + integrity sha512-S4B/zQNSuYc0M/1Wq8dJDTIO9yRgU0ZwDGnmlqxGGmFombOZb9mLjylewSfQKmjNpciZ7iUIBbJ0mHlPJTzdXg== + dependencies: + apollo-cache "^1.3.2" + apollo-utilities "^1.3.2" + optimism "^0.10.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + +apollo-cache@1.3.2, apollo-cache@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.2.tgz#df4dce56240d6c95c613510d7e409f7214e6d26a" + integrity sha512-+KA685AV5ETEJfjZuviRTEImGA11uNBp/MJGnaCvkgr+BYRrGLruVKBv6WvyFod27WEB2sp7SsG8cNBKANhGLg== + dependencies: + apollo-utilities "^1.3.2" + tslib "^1.9.3" + +apollo-client@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.4.tgz#872c32927263a0d34655c5ef8a8949fbb20b6140" + integrity sha512-oWOwEOxQ9neHHVZrQhHDbI6bIibp9SHgxaLRVPoGvOFy7OH5XUykZE7hBQAVxq99tQjBzgytaZffQkeWo1B4VQ== + dependencies: + "@types/zen-observable" "^0.8.0" + apollo-cache "1.3.2" + apollo-link "^1.0.0" + apollo-utilities "1.3.2" + symbol-observable "^1.0.2" + ts-invariant "^0.4.0" + tslib "^1.9.3" + zen-observable "^0.8.0" + +apollo-codegen-core@^0.35.7: + version "0.35.7" + resolved "https://registry.yarnpkg.com/apollo-codegen-core/-/apollo-codegen-core-0.35.7.tgz#665b70c5ed5be0acb0cbde64f6e439b8cc0389a8" + integrity sha512-Aj8bzwUnnsILaOxjPLJKpkzb4yNNQCpY21Atj0NZYGi6Zzfl9EK/PZn7BE2/4pyNpomErjd2C7X3m9bdvndTAg== + dependencies: + "@babel/generator" "7.6.4" + "@babel/parser" "^7.1.3" + "@babel/types" "7.6.3" + apollo-env "^0.5.1" + apollo-language-server "^1.17.0" + ast-types "^0.13.0" + common-tags "^1.5.1" + recast "^0.18.0" + +apollo-codegen-flow@^0.33.32: + version "0.33.32" + resolved "https://registry.yarnpkg.com/apollo-codegen-flow/-/apollo-codegen-flow-0.33.32.tgz#8c26156357726fb8b4d7a3befec495b9eb78ae48" + integrity sha512-/DWh6/8/BKYlwPg5/kQQQU78skC3TCRxhg4rUaR0s23px/N3YiLCcqmcV6HzcMKpjNE+dIxpha+WLNMAtb4ypg== + dependencies: + "@babel/generator" "7.6.4" + "@babel/types" "7.6.3" + apollo-codegen-core "^0.35.7" + apollo-env "^0.5.1" + change-case "^3.0.1" + common-tags "^1.5.1" + inflected "^2.0.3" + +apollo-codegen-scala@^0.34.32: + version "0.34.32" + resolved "https://registry.yarnpkg.com/apollo-codegen-scala/-/apollo-codegen-scala-0.34.32.tgz#6f34b2c8a053bcc31062b269dda5a59d2df3802e" + integrity sha512-c41a6NdEsPo/uRSY1FBbEJ2MUfM+NKlLVe3hBulc3huqaStz3PuQuOtQ5ZA0XjCwL8GhLFgC1D8TAiuDCA5+tQ== + dependencies: + apollo-codegen-core "^0.35.7" + apollo-env "^0.5.1" + change-case "^3.0.1" + common-tags "^1.5.1" + inflected "^2.0.3" + +apollo-codegen-swift@^0.35.12: + version "0.35.12" + resolved "https://registry.yarnpkg.com/apollo-codegen-swift/-/apollo-codegen-swift-0.35.12.tgz#51033aa2bb43c3e369a4ee634536200728ff81aa" + integrity sha512-pbeUwkLEpfr/hkCLnwmxfxroib5QFP0xz7cvORN3Hh9xxtpqfd0SdnrbXS8RpRxBN2jTbV78ubA6hDFm4B0BCA== + dependencies: + apollo-codegen-core "^0.35.7" + apollo-env "^0.5.1" + change-case "^3.0.1" + common-tags "^1.5.1" + inflected "^2.0.3" + +apollo-codegen-typescript@^0.35.7: + version "0.35.7" + resolved "https://registry.yarnpkg.com/apollo-codegen-typescript/-/apollo-codegen-typescript-0.35.7.tgz#fbfaaed7883b01433e91ae6852bf1c83a8b89894" + integrity sha512-r5MzXwWVPvkuyWfUV5kWzIsezoiDw2o459L3i07iM2JwOl7AW88/wkPsp/GrKh5q/Y2HRABg4oSqEiaQtrVazQ== + dependencies: + "@babel/generator" "7.6.4" + "@babel/types" "7.6.3" + apollo-codegen-core "^0.35.7" + apollo-env "^0.5.1" + change-case "^3.0.1" + common-tags "^1.5.1" + inflected "^2.0.3" + +apollo-datasource@^0.6.0: + version "0.6.3" + resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.6.3.tgz#b31e089e52adb92fabb536ab8501c502573ffe13" + integrity sha512-gRYyFVpJgHE2hhS+VxMeOerxXQ/QYxWG7T6QddfugJWYAG9DRCl65e2b7txcGq2NP3r+O1iCm4GNwhRBDJbd8A== + dependencies: + apollo-server-caching "^0.5.0" + apollo-server-env "^2.4.3" + +apollo-env@0.5.1, apollo-env@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.5.1.tgz#b9b0195c16feadf0fe9fd5563edb0b9b7d9e97d3" + integrity sha512-fndST2xojgSdH02k5hxk1cbqA9Ti8RX4YzzBoAB4oIe1Puhq7+YlhXGXfXB5Y4XN0al8dLg+5nAkyjNAR2qZTw== + dependencies: + core-js "^3.0.1" + node-fetch "^2.2.0" + sha.js "^2.4.11" + +apollo-graphql@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.3.4.tgz#c1f68591a4775945441d049eff9323542ab0401f" + integrity sha512-w+Az1qxePH4oQ8jvbhQBl5iEVvqcqynmU++x/M7MM5xqN1C7m1kyIzpN17gybXlTJXY4Oxej2WNURC2/hwpfYw== + dependencies: + apollo-env "^0.5.1" + lodash.sortby "^4.7.0" + +apollo-language-server@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/apollo-language-server/-/apollo-language-server-1.17.0.tgz#4f933dd4d2eb18869cacb79a6b875f268e42f882" + integrity sha512-x/CfKYl+GSFGLxLDx6d3aWLzpQssnHUdwczZxTHyU/RkQlfnNyLdZXodCmWBRpSj2Ifkuzii+uxwj4kjWG6jOA== + dependencies: + "@apollo/federation" "0.10.2" + "@apollographql/apollo-tools" "^0.4.0" + "@apollographql/graphql-language-service-interface" "^2.0.2" + "@endemolshinegroup/cosmiconfig-typescript-loader" "^1.0.0" + apollo-datasource "^0.6.0" + apollo-env "^0.5.1" + apollo-graphql "^0.3.4" + apollo-link "^1.2.3" + apollo-link-context "^1.0.9" + apollo-link-error "^1.1.1" + apollo-link-http "^1.5.5" + apollo-server-errors "^2.0.2" + await-to-js "^2.0.1" + core-js "^3.0.1" + cosmiconfig "^5.0.6" + dotenv "^8.0.0" + glob "^7.1.3" + graphql "14.0.2 - 14.2.0 || ^14.3.1" + graphql-tag "^2.10.1" + lodash.debounce "^4.0.8" + lodash.merge "^4.6.1" + minimatch "^3.0.4" + moment "^2.24.0" + vscode-languageserver "^5.1.0" + vscode-uri "1.0.6" + +apollo-link-context@^1.0.9: + version "1.0.19" + resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.19.tgz#3c9ba5bf75ed5428567ce057b8837ef874a58987" + integrity sha512-TUi5TyufU84hEiGkpt+5gdH5HkB3Gx46npNfoxR4of3DKBCMuItGERt36RCaryGcU/C3u2zsICU3tJ+Z9LjFoQ== + dependencies: + apollo-link "^1.2.13" + tslib "^1.9.3" + +apollo-link-error@^1.0.3: + version "1.1.10" + resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.10.tgz#ce57f0793f0923b598655de5bf5e028d4cf4fba6" + integrity sha512-itG5UV7mQqaalmRkuRsF0cUS4zW2ja8XCbxkMZnIEeN24X3yoJi5hpJeAaEkXf0KgYNsR0+rmtCQNruWyxDnZQ== + dependencies: + apollo-link "^1.2.11" + apollo-link-http-common "^0.2.13" + tslib "^1.9.3" + +apollo-link-error@^1.1.1: + version "1.1.12" + resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.12.tgz#e24487bb3c30af0654047611cda87038afbacbf9" + integrity sha512-psNmHyuy3valGikt/XHJfe0pKJnRX19tLLs6P6EHRxg+6q6JMXNVLYPaQBkL0FkwdTCB0cbFJAGRYCBviG8TDA== + dependencies: + apollo-link "^1.2.13" + apollo-link-http-common "^0.2.15" + tslib "^1.9.3" + +apollo-link-http-common@^0.2.13, apollo-link-http-common@^0.2.15: + version "0.2.15" + resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.15.tgz#304e67705122bf69a9abaded4351b10bc5efd6d9" + integrity sha512-+Heey4S2IPsPyTf8Ag3PugUupASJMW894iVps6hXbvwtg1aHSNMXUYO5VG7iRHkPzqpuzT4HMBanCTXPjtGzxg== + dependencies: + apollo-link "^1.2.13" + ts-invariant "^0.4.0" + tslib "^1.9.3" + +apollo-link-http@^1.3.1: + version "1.5.14" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.14.tgz#ed6292248d1819ccd16523e346d35203a1b31109" + integrity sha512-XEoPXmGpxFG3wioovgAlPXIarWaW4oWzt8YzjTYZ87R4R7d1A3wKR/KcvkdMV1m5G7YSAHcNkDLe/8hF2nH6cg== + dependencies: + apollo-link "^1.2.11" + apollo-link-http-common "^0.2.13" + tslib "^1.9.3" + +apollo-link-http@^1.5.5: + version "1.5.16" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.16.tgz#44fe760bcc2803b8a7f57fc9269173afb00f3814" + integrity sha512-IA3xA/OcrOzINRZEECI6IdhRp/Twom5X5L9jMehfzEo2AXdeRwAMlH5LuvTZHgKD8V1MBnXdM6YXawXkTDSmJw== + dependencies: + apollo-link "^1.2.13" + apollo-link-http-common "^0.2.15" + tslib "^1.9.3" + +apollo-link@^1.0.0, apollo-link@^1.0.6: + version "1.2.11" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.11.tgz#493293b747ad3237114ccd22e9f559e5e24a194d" + integrity sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA== + dependencies: + apollo-utilities "^1.2.1" + ts-invariant "^0.3.2" + tslib "^1.9.3" + zen-observable-ts "^0.8.18" + +apollo-link@^1.2.11, apollo-link@^1.2.13, apollo-link@^1.2.3: + version "1.2.13" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.13.tgz#dff00fbf19dfcd90fddbc14b6a3f9a771acac6c4" + integrity sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw== + dependencies: + apollo-utilities "^1.3.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + zen-observable-ts "^0.8.20" + +apollo-server-caching@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.0.tgz#446a37ce2d4e24c81833e276638330a634f7bd46" + integrity sha512-l7ieNCGxUaUAVAAp600HjbUJxVaxjJygtPV0tPTe1Q3HkPy6LEWoY6mNHV7T268g1hxtPTxcdRu7WLsJrg7ufw== + dependencies: + lru-cache "^5.0.0" + +apollo-server-env@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.3.tgz#9bceedaae07eafb96becdfd478f8d92617d825d2" + integrity sha512-23R5Xo9OMYX0iyTu2/qT0EUb+AULCBriA9w8HDfMoChB8M+lFClqUkYtaTTHDfp6eoARLW8kDBhPOBavsvKAjA== + dependencies: + node-fetch "^2.1.2" + util.promisify "^1.0.0" + +apollo-server-errors@^2.0.2: + version "2.3.4" + resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.4.tgz#b70ef01322f616cbcd876f3e0168a1a86b82db34" + integrity sha512-Y0PKQvkrb2Kd18d1NPlHdSqmlr8TgqJ7JQcNIfhNDgdb45CnqZlxL1abuIRhr8tiw8OhVOcFxz2KyglBi8TKdA== + +apollo-utilities@1.3.2, apollo-utilities@^1.2.1, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" + integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg== + dependencies: + "@wry/equality" "^0.1.2" + fast-json-stable-stringify "^2.0.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + +apollo@^2.21.0: + version "2.21.0" + resolved "https://registry.yarnpkg.com/apollo/-/apollo-2.21.0.tgz#a456315d4e22801c807ed4ff526e8bb28e2534a4" + integrity sha512-S0PzWA6Ja3E68/LjG3D4wbOW5BiWo+PdST/TZD7n4TjRKV4eeMONEd95OSv5OzMMYaLtHSRq8xZmQEDe4l0NsA== + dependencies: + "@apollographql/apollo-tools" "^0.4.0" + "@oclif/command" "1.5.19" + "@oclif/config" "1.13.3" + "@oclif/errors" "1.2.2" + "@oclif/plugin-autocomplete" "0.1.4" + "@oclif/plugin-help" "2.2.1" + "@oclif/plugin-not-found" "1.2.3" + "@oclif/plugin-plugins" "1.7.8" + "@oclif/plugin-warn-if-update-available" "1.7.0" + apollo-codegen-core "^0.35.7" + apollo-codegen-flow "^0.33.32" + apollo-codegen-scala "^0.34.32" + apollo-codegen-swift "^0.35.12" + apollo-codegen-typescript "^0.35.7" + apollo-env "^0.5.1" + apollo-graphql "^0.3.4" + apollo-language-server "^1.17.0" + chalk "2.4.2" + env-ci "3.2.2" + gaze "1.1.3" + git-parse "1.0.3" + git-rev-sync "1.12.0" + glob "7.1.4" + graphql "14.0.2 - 14.2.0 || ^14.3.1" + graphql-tag "2.10.1" + listr "0.14.3" + lodash.identity "3.0.0" + lodash.pickby "4.6.0" + moment "2.24.0" + strip-ansi "5.2.0" + table "5.4.6" + tty "1.0.1" + vscode-uri "1.0.6" + +aproba@^1.0.3, aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arg@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.1.tgz#485f8e7c390ce4c5f78257dbea80d4be11feda4c" + integrity sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +aria-query@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" + integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w= + dependencies: + ast-types-flow "0.0.7" + commander "^2.11.0" + +arity-n@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745" + integrity sha1-2edrEXM+CFacCEeuezmyhgswt0U= + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-includes@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +array.prototype.find@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.0.tgz#630f2eaf70a39e608ac3573e45cf8ccd0ede9ad7" + integrity sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.13.0" + +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +asap@~2.0.3, asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1.js@^4.0.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assert@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE= + dependencies: + util "0.10.3" + +assert@^1.1.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" + integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== + dependencies: + object-assign "^4.1.1" + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +ast-types-flow@0.0.7, ast-types-flow@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" + integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= + +ast-types@0.13.2, ast-types@^0.13.0: + version "0.13.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48" + integrity sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA== + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +autoprefixer@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.1.tgz#51967a02d2d2300bb01866c1611ec8348d355a47" + integrity sha512-aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw== + dependencies: + browserslist "^4.6.3" + caniuse-lite "^1.0.30000980" + chalk "^2.4.2" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.17" + postcss-value-parser "^4.0.0" + +await-to-js@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/await-to-js/-/await-to-js-2.1.1.tgz#c2093cd5a386f2bb945d79b292817bbc3f41b31b" + integrity sha512-CHBC6gQGCIzjZ09tJ+XmpQoZOn4GdWePB4qUweCaKNJ0D3f115YdhmYVTZ4rMVpiJ3cFzZcTYK1VMYEICV4YXw== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +axobject-query@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" + integrity sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww== + dependencies: + ast-types-flow "0.0.7" + +babel-code-frame@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-eslint@10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a" + integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.0.0" + "@babel/traverse" "^7.0.0" + "@babel/types" "^7.0.0" + eslint-visitor-keys "^1.0.0" + resolve "^1.12.0" + +babel-extract-comments@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/babel-extract-comments/-/babel-extract-comments-1.0.0.tgz#0a2aedf81417ed391b85e18b4614e693a0351a21" + integrity sha512-qWWzi4TlddohA91bFwgt6zO/J0X+io7Qp184Fw0m2JYRSTZnJbFR8+07KmzudHCZgOiKRCrjhylwv9Xd8gfhVQ== + dependencies: + babylon "^6.18.0" + +babel-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" + integrity sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw== + dependencies: + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/babel__core" "^7.1.0" + babel-plugin-istanbul "^5.1.0" + babel-preset-jest "^24.9.0" + chalk "^2.4.2" + slash "^2.0.0" + +babel-loader@8.0.6: + version "8.0.6" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb" + integrity sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw== + dependencies: + find-cache-dir "^2.0.0" + loader-utils "^1.0.2" + mkdirp "^0.5.1" + pify "^4.0.1" + +babel-plugin-dynamic-import-node@2.3.0, babel-plugin-dynamic-import-node@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" + integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ== + dependencies: + object.assign "^4.1.0" + +babel-plugin-istanbul@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854" + integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + find-up "^3.0.0" + istanbul-lib-instrument "^3.3.0" + test-exclude "^5.2.3" + +babel-plugin-jest-hoist@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756" + integrity sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw== + dependencies: + "@types/babel__traverse" "^7.0.6" + +babel-plugin-macros@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.6.1.tgz#41f7ead616fc36f6a93180e89697f69f51671181" + integrity sha512-6W2nwiXme6j1n2erPOnmRiWfObUhWH7Qw1LMi9XZy8cj+KtESu3T6asZvtk5bMQQjX8te35o7CFueiSdL/2NmQ== + dependencies: + "@babel/runtime" "^7.4.2" + cosmiconfig "^5.2.0" + resolve "^1.10.0" + +babel-plugin-named-asset-import@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.4.tgz#4a8fc30e9a3e2b1f5ed36883386ab2d84e1089bd" + integrity sha512-S6d+tEzc5Af1tKIMbsf2QirCcPdQ+mKUCY2H1nJj1DyA1ShwpsoxEOAwbWsG5gcXNV/olpvQd9vrUWRx4bnhpw== + +babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U= + +babel-plugin-transform-object-rest-spread@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + integrity sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY= + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + +babel-plugin-transform-react-remove-prop-types@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" + integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== + +babel-polyfill@6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" + integrity sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM= + dependencies: + babel-runtime "^6.26.0" + core-js "^2.5.0" + regenerator-runtime "^0.10.5" + +babel-preset-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc" + integrity sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg== + dependencies: + "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + babel-plugin-jest-hoist "^24.9.0" + +babel-preset-react-app@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-9.0.2.tgz#247d37e883d6d6f4b4691e5f23711bb2dd80567d" + integrity sha512-aXD+CTH8Chn8sNJr4tO/trWKqe5sSE4hdO76j9fhVezJSzmpWYWUSc5JoPmdSxADwef5kQFNGKXd433vvkd2VQ== + dependencies: + "@babel/core" "7.6.0" + "@babel/plugin-proposal-class-properties" "7.5.5" + "@babel/plugin-proposal-decorators" "7.6.0" + "@babel/plugin-proposal-object-rest-spread" "7.5.5" + "@babel/plugin-syntax-dynamic-import" "7.2.0" + "@babel/plugin-transform-destructuring" "7.6.0" + "@babel/plugin-transform-flow-strip-types" "7.4.4" + "@babel/plugin-transform-react-display-name" "7.2.0" + "@babel/plugin-transform-runtime" "7.6.0" + "@babel/preset-env" "7.6.0" + "@babel/preset-react" "7.0.0" + "@babel/preset-typescript" "7.6.0" + "@babel/runtime" "7.6.0" + babel-plugin-dynamic-import-node "2.3.0" + babel-plugin-macros "2.6.1" + babel-plugin-transform-react-remove-prop-types "0.4.24" + +babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== + +balanced-match@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + integrity sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg= + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +bluebird@^3.5.5: + version "3.5.5" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" + integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== + +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + +browser-process-hrtime@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4" + integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw== + +browser-resolve@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" + integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== + dependencies: + resolve "1.1.7" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg= + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + +browserslist@4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.7.0.tgz#9ee89225ffc07db03409f2fee524dc8227458a17" + integrity sha512-9rGNDtnj+HaahxiVV38Gn8n8Lr8REKsel68v1sPFfIGEK6uSXTY3h9acgiT1dZVtOOUtifo/Dn8daDQ5dUgVsA== + dependencies: + caniuse-lite "^1.0.30000989" + electron-to-chromium "^1.3.247" + node-releases "^1.1.29" + +browserslist@^4.0.0, browserslist@^4.1.1, browserslist@^4.6.0, browserslist@^4.6.3, browserslist@^4.6.4, browserslist@^4.6.6: + version "4.6.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.6.tgz#6e4bf467cde520bc9dbdf3747dafa03531cec453" + integrity sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA== + dependencies: + caniuse-lite "^1.0.30000984" + electron-to-chromium "^1.3.191" + node-releases "^1.1.25" + +bser@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.0.tgz#65fc784bf7f87c009b973c12db6546902fa9c7b5" + integrity sha512-8zsjWrQkkBoLK6uxASk1nJ2SKv97ltiGDo6A3wA0/yRPz+CwmEyDo0hUrhIuukG2JHpAl3bvFIixw2/3Hi0DOg== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= + +byline@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cacache@^12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.2.tgz#8db03205e36089a3df6954c66ce92541441ac46c" + integrity sha512-ifKgxH2CKhJEg6tNdAwziu6Q33EvuG26tYcda6PT3WKisZcYDXsnEdnRv67Po3yCzFfaSoMjGZzJyD2c3DT1dg== + dependencies: + bluebird "^3.5.5" + chownr "^1.1.1" + figgy-pudding "^3.5.1" + glob "^7.1.4" + graceful-fs "^4.1.15" + infer-owner "^1.0.3" + lru-cache "^5.1.1" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.3" + ssri "^6.0.1" + unique-filename "^1.1.1" + y18n "^4.0.0" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +call-me-maybe@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" + integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + dependencies: + caller-callsite "^2.0.0" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@3.0.x, camel-case@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + +camelcase@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" + integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= + +camelcase@^5.0.0, camelcase@^5.2.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30000984: + version "1.0.30000989" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz#b9193e293ccf7e4426c5245134b8f2a56c0ac4b9" + integrity sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw== + +caniuse-lite@^1.0.30000989: + version "1.0.30001010" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001010.tgz#397a14034d384260453cc81994f494626d34b938" + integrity sha512-RA5GH9YjFNea4ZQszdWgh2SC+dpLiRAg4VDQS2b5JRI45OxmbGrYocYHTa9x0bKMQUE7uvHkNPNffUr+pCxSGw== + +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + dependencies: + rsvp "^4.8.4" + +cardinal@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-2.1.1.tgz#7cc1055d822d212954d07b085dea251cc7bc5505" + integrity sha1-fMEFXYItISlU0HsIXeolHMe8VQU= + dependencies: + ansicolors "~0.3.2" + redeyed "~2.1.0" + +case-sensitive-paths-webpack-plugin@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.2.0.tgz#3371ef6365ef9c25fa4b81c16ace0e9c7dc58c3e" + integrity sha512-u5ElzokS8A1pm9vM3/iDgTcI3xqHxuCao94Oz8etI3cf0Tio0p8izkDYbTIn09uP3yUUr6+veaE6IkjnTYS46g== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^1.0.0, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +change-case@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.1.0.tgz#0e611b7edc9952df2e8513b27b42de72647dd17e" + integrity sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw== + dependencies: + camel-case "^3.0.0" + constant-case "^2.0.0" + dot-case "^2.1.0" + header-case "^1.0.0" + is-lower-case "^1.1.0" + is-upper-case "^1.1.0" + lower-case "^1.1.1" + lower-case-first "^1.0.0" + no-case "^2.3.2" + param-case "^2.1.0" + pascal-case "^2.0.0" + path-case "^2.1.0" + sentence-case "^2.1.0" + snake-case "^2.1.0" + swap-case "^1.1.0" + title-case "^2.1.0" + upper-case "^1.1.1" + upper-case-first "^1.1.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4: + version "2.1.6" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.6.tgz#b6cad653a929e244ce8a834244164d241fa954c5" + integrity sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chownr@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6" + integrity sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A== + +chrome-trace-event@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" + integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== + dependencies: + tslib "^1.9.0" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +classnames@2.x, classnames@^2.2.5, classnames@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" + integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== + +clean-css@4.2.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17" + integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g== + dependencies: + source-map "~0.6.0" + +clean-stack@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-1.3.0.tgz#9e821501ae979986c46b1d66d2d432db2fd4ae31" + integrity sha1-noIVAa6XmYbEax1m0tQy2y/UrjE= + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^2.0.0, cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= + dependencies: + restore-cursor "^2.0.0" + +cli-truncate@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" + integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ= + dependencies: + slice-ansi "0.0.4" + string-width "^1.0.1" + +cli-ux@^4.4.0, cli-ux@^4.9.0: + version "4.9.3" + resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-4.9.3.tgz#4c3e070c1ea23eef010bbdb041192e0661be84ce" + integrity sha512-/1owvF0SZ5Gn54cgrikJ0QskgTzeg30HGjkmjFoaHDJzAqFpuX1DBpFR8aLvsE1J5s9MgeYRENQK4BFwOag5VA== + dependencies: + "@oclif/errors" "^1.2.2" + "@oclif/linewrap" "^1.0.0" + "@oclif/screen" "^1.0.3" + ansi-escapes "^3.1.0" + ansi-styles "^3.2.1" + cardinal "^2.1.1" + chalk "^2.4.1" + clean-stack "^2.0.0" + extract-stack "^1.0.0" + fs-extra "^7.0.0" + hyperlinker "^1.0.0" + indent-string "^3.2.0" + is-wsl "^1.1.0" + lodash "^4.17.11" + password-prompt "^1.0.7" + semver "^5.6.0" + strip-ansi "^5.0.0" + supports-color "^5.5.0" + supports-hyperlinks "^1.0.1" + treeify "^1.1.0" + tslib "^1.9.3" + +cli-ux@^5.2.1: + version "5.3.3" + resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-5.3.3.tgz#6459e180da29f2850473b9bf2f1ae097e5257d31" + integrity sha512-a16g+BTjASUH41s1pevai4P3JKwhx85wkOSm6sXWsk6KkdSmDeJ16pSCn2x3nqK7W8n35igOu2YiW+qFkqLRJg== + dependencies: + "@oclif/command" "^1.5.1" + "@oclif/errors" "^1.2.1" + "@oclif/linewrap" "^1.0.0" + "@oclif/screen" "^1.0.3" + ansi-escapes "^3.1.0" + ansi-styles "^3.2.1" + cardinal "^2.1.1" + chalk "^2.4.1" + clean-stack "^2.0.0" + extract-stack "^1.0.0" + fs-extra "^7.0.1" + hyperlinker "^1.0.0" + indent-string "^3.2.0" + is-wsl "^1.1.0" + lodash "^4.17.11" + natural-orderby "^2.0.1" + password-prompt "^1.1.2" + semver "^5.6.0" + string-width "^3.1.0" + strip-ansi "^5.1.0" + supports-color "^5.5.0" + supports-hyperlinks "^1.0.1" + treeify "^1.1.0" + tslib "^1.9.3" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +clone-deep@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.2.4.tgz#4e73dd09e9fb971cc38670c5dced9c1896481cc6" + integrity sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY= + dependencies: + for-own "^0.1.3" + is-plain-object "^2.0.1" + kind-of "^3.0.2" + lazy-cache "^1.0.3" + shallow-clone "^0.1.2" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clsx@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.0.3.tgz#e164004f4064b372888f20fdafbd436fb960bac9" + integrity sha512-xLoSw6DMp7YvbEeLrQJBcWWRRerdHrU1WHoL1hYJOKUeDpVMRq7pv7NI2JHQbCRAe5ptINNzhdYmtfN6MsdCUw== + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" + integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@~0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" + integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0= + +color-hash@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/color-hash/-/color-hash-1.0.3.tgz#c0e7952f06d022e548e65da239512bd67d3809ee" + integrity sha1-wOeVLwbQIuVI5l2iOVEr1n04Ce4= + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" + integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@2.17.x: + version "2.17.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== + +commander@^2.11.0, commander@^2.20.0, commander@~2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + +commander@^2.12.1, commander@~2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + +common-tags@^1.5.1, common-tags@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" + integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +compose-function@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f" + integrity sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8= + dependencies: + arity-n "^1.0.4" + +compressible@~2.0.16: + version "2.0.17" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1" + integrity sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw== + dependencies: + mime-db ">= 1.40.0 < 2" + +compression@^1.5.2: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +compute-scroll-into-view@^1.0.9: + version "1.0.11" + resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.11.tgz#7ff0a57f9aeda6314132d8994cce7aeca794fecf" + integrity sha512-uUnglJowSe0IPmWOdDtrlHXof5CTIJitfJEyITHBW6zDVOGu9Pjk5puaLM73SLcwak0L4hEjO7Td88/a6P5i7A== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +confusing-browser-globals@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz#72bc13b483c0276801681871d4898516f8f54fdd" + integrity sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw== + +connect-history-api-fallback@^1.3.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA= + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +constant-case@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46" + integrity sha1-QXV2TTidP6nI7NKRhu1gBSQ7akY= + dependencies: + snake-case "^2.1.0" + upper-case "^1.1.1" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= + +contains-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@^1.0.4, content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-css-length@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/convert-css-length/-/convert-css-length-2.0.1.tgz#90a76bde5bfd24d72881a5b45d02249b2c1d257c" + integrity sha512-iGpbcvhLPRKUbBc0Quxx7w/bV14AC3ItuBEGMahA5WTYqB8lq9jH0kTXFheCBASsYnqeMFZhiTruNxr1N59Axg== + +convert-source-map@1.6.0, convert-source-map@^1.1.0, convert-source-map@^1.4.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== + dependencies: + safe-buffer "~5.1.1" + +convert-source-map@^0.3.3: + version "0.3.5" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" + integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-js-compat@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.2.0.tgz#d7fcc4d695d66b069437bd9d9f411274ceb196d3" + integrity sha512-vQve8j3tGPPqIkrsK442hbVwDIGXcngLPnislJaCNJIG1aBWPD0IqRhpxEwjtUOYpzLap8xA7CoGlJBT1ZkcDA== + dependencies: + browserslist "^4.6.6" + semver "^6.3.0" + +core-js@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" + integrity sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw== + +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= + +core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.10: + version "2.6.10" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.10.tgz#8a5b8391f8cc7013da703411ce5b585706300d7f" + integrity sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA== + +core-js@^3.0.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.4.1.tgz#76dd6828412900ab27c8ce0b22e6114d7ce21b18" + integrity sha512-KX/dnuY/J8FtEwbnrzmAjUYgLqtk+cxM86hfG60LGiW3MmltIc2yAmDgBgEkfm0blZhUrdr1Zd84J2Y14mLxzg== + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cosmiconfig@^5.0.0, cosmiconfig@^5.0.6, cosmiconfig@^5.2.0, cosmiconfig@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +create-ecdh@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" + integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw== + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +css-blank-pseudo@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" + integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w== + dependencies: + postcss "^7.0.5" + +css-color-names@0.0.4, css-color-names@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= + +css-declaration-sorter@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" + integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== + dependencies: + postcss "^7.0.1" + timsort "^0.3.0" + +css-has-pseudo@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee" + integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^5.0.0-rc.4" + +css-loader@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea" + integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w== + dependencies: + camelcase "^5.2.0" + icss-utils "^4.1.0" + loader-utils "^1.2.3" + normalize-path "^3.0.0" + postcss "^7.0.14" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^2.0.6" + postcss-modules-scope "^2.1.0" + postcss-modules-values "^2.0.0" + postcss-value-parser "^3.3.0" + schema-utils "^1.0.0" + +css-prefers-color-scheme@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4" + integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg== + dependencies: + postcss "^7.0.5" + +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== + +css-select@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-select@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.0.2.tgz#ab4386cec9e1f668855564b17c3733b43b2a5ede" + integrity sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ== + dependencies: + boolbase "^1.0.0" + css-what "^2.1.2" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-tree@1.0.0-alpha.29: + version "1.0.0-alpha.29" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39" + integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg== + dependencies: + mdn-data "~1.1.0" + source-map "^0.5.3" + +css-tree@1.0.0-alpha.33: + version "1.0.0-alpha.33" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.33.tgz#970e20e5a91f7a378ddd0fc58d0b6c8d4f3be93e" + integrity sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w== + dependencies: + mdn-data "2.0.4" + source-map "^0.5.3" + +css-unit-converter@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" + integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY= + +css-vendor@^2.0.6: + version "2.0.7" + resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.7.tgz#4e6d53d953c187981576d6a542acc9fb57174bda" + integrity sha512-VS9Rjt79+p7M0WkPqcAza4Yq1ZHrsHrwf7hPL/bjQB+c1lwmAI+1FXxYTYt818D/50fFVflw0XKleiBN5RITkg== + dependencies: + "@babel/runtime" "^7.6.2" + is-in-browser "^1.0.2" + +css-what@2.1, css-what@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== + +css@^2.0.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" + integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== + dependencies: + inherits "^2.0.3" + source-map "^0.6.1" + source-map-resolve "^0.5.2" + urix "^0.1.0" + +cssdb@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" + integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ== + +cssesc@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" + integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" + integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA== + dependencies: + css-declaration-sorter "^4.0.1" + cssnano-util-raw-cache "^4.0.1" + postcss "^7.0.0" + postcss-calc "^7.0.1" + postcss-colormin "^4.0.3" + postcss-convert-values "^4.0.1" + postcss-discard-comments "^4.0.2" + postcss-discard-duplicates "^4.0.2" + postcss-discard-empty "^4.0.1" + postcss-discard-overridden "^4.0.1" + postcss-merge-longhand "^4.0.11" + postcss-merge-rules "^4.0.3" + postcss-minify-font-values "^4.0.2" + postcss-minify-gradients "^4.0.2" + postcss-minify-params "^4.0.2" + postcss-minify-selectors "^4.0.2" + postcss-normalize-charset "^4.0.1" + postcss-normalize-display-values "^4.0.2" + postcss-normalize-positions "^4.0.2" + postcss-normalize-repeat-style "^4.0.2" + postcss-normalize-string "^4.0.2" + postcss-normalize-timing-functions "^4.0.2" + postcss-normalize-unicode "^4.0.1" + postcss-normalize-url "^4.0.1" + postcss-normalize-whitespace "^4.0.2" + postcss-ordered-values "^4.1.2" + postcss-reduce-initial "^4.0.3" + postcss-reduce-transforms "^4.0.2" + postcss-svgo "^4.0.2" + postcss-unique-selectors "^4.0.1" + +cssnano-util-get-arguments@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" + integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8= + +cssnano-util-get-match@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" + integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0= + +cssnano-util-raw-cache@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" + integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== + dependencies: + postcss "^7.0.0" + +cssnano-util-same-parent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" + integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== + +cssnano@^4.1.10: + version "4.1.10" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" + integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ== + dependencies: + cosmiconfig "^5.0.0" + cssnano-preset-default "^4.0.7" + is-resolvable "^1.0.0" + postcss "^7.0.0" + +csso@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" + integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg== + dependencies: + css-tree "1.0.0-alpha.29" + +cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0", cssom@^0.3.4: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^1.0.0, cssstyle@^1.1.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" + integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== + dependencies: + cssom "0.3.x" + +csstype@^2.2.0, csstype@^2.6.5, csstype@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5" + integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ== + +csstype@^2.5.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.3.tgz#b701e5968245bf9b08d54ac83d00b624e622a9fa" + integrity sha512-rINUZXOkcBmoHWEyu7JdHu5JMzkGRoMX4ov9830WNgxf5UYxcBUO0QTKAqeJ5EZfSdlrcJYkC8WwfVW7JYi4yg== + +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= + +d3-array@^1.2.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" + integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== + +d3-collection@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e" + integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A== + +d3-color@1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.3.tgz#6c67bb2af6df3cc8d79efcc4d3a3e83e28c8048f" + integrity sha512-x37qq3ChOTLd26hnps36lexMRhNXEtVxZ4B25rL0DVdDsGQIJGB18S7y9XDwlDD6MD/ZBzITCf4JjGMM10TZkw== + +d3-format@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.2.tgz#6a96b5e31bcb98122a30863f7d92365c00603562" + integrity sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ== + +d3-interpolate@1, d3-interpolate@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.3.2.tgz#417d3ebdeb4bc4efcc8fd4361c55e4040211fd68" + integrity sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w== + dependencies: + d3-color "1" + +d3-path@1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.7.tgz#8de7cd693a75ac0b5480d3abaccd94793e58aae8" + integrity sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA== + +d3-scale@^2.1.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" + integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + +d3-shape@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.5.tgz#e81aea5940f59f0a79cfccac012232a8987c6033" + integrity sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg== + dependencies: + d3-path "1" + +d3-time-format@2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.3.tgz#ae06f8e0126a9d60d6364eac5b1533ae1bac826b" + integrity sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA== + dependencies: + d3-time "1" + +d3-time@1: + version "1.0.11" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.11.tgz#1d831a3e25cd189eb256c17770a666368762bbce" + integrity sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw== + +d@1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + +damerau-levenshtein@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz#780cf7144eb2e8dbd1c3bb83ae31100ccc31a414" + integrity sha512-CBCRqFnpu715iPmw1KrdOrzRqbdFwQTwAWyyyYS42+iAgHCuXZ+/TdMgQkUENPomxEz9z1BEzuQU2Xw0kUuAgA== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +data-urls@^1.0.0, data-urls@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" + integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== + dependencies: + abab "^2.0.0" + whatwg-mimetype "^2.2.0" + whatwg-url "^7.0.0" + +date-fns@^1.27.2: + version "1.30.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" + integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.1.0, debug@^3.2.5, debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decamelize@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7" + integrity sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg== + dependencies: + xregexp "4.0.0" + +decimal.js-light@^2.4.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.0.tgz#ca7faf504c799326df94b0ab920424fdfc125348" + integrity sha512-b3VJCbd2hwUpeRGG3Toob+CRo8W22xplipNhP3tN7TSVB/cyMX71P1vM2Xjc9H74uV6dS2hDDmo/rHq8L87Upg== + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +default-gateway@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== + dependencies: + execa "^1.0.0" + ip-regex "^2.1.0" + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + integrity sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU= + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + integrity sha1-wHTS4qpqipoH29YfmhXCzYPsjsw= + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= + +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + +detect-port-alt@1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" + integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== + dependencies: + address "^1.0.1" + debug "^2.6.0" + +diff-sequences@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" + integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== + +diff@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" + integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dir-glob@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" + integrity sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag== + dependencies: + arrify "^1.0.1" + path-type "^3.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" + integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + +doctrine@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-converter@^0.2: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-helpers@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + +dom-helpers@^5.0.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" + integrity sha512-nZD1OtwfWGRBWlpANxacBEZrEuLa16o1nh7YopFWeoF68Zt8GGEmzHu6Xv4F3XaFIC+YXtTLrzgqKxFgLEe4jw== + dependencies: + "@babel/runtime" "^7.6.3" + csstype "^2.6.7" + +dom-serializer@0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.1.tgz#13650c850daffea35d8b626a4cfc4d3a17643fdb" + integrity sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== + +domelementtype@1, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" + integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1, domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-2.1.1.tgz#34dcf37f50a8e93c2b3bca8bb7fb9155c7da3bee" + integrity sha1-NNzzf1Co6TwrO8qLt/uRVcfaO+4= + dependencies: + no-case "^2.2.0" + +dot-prop@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== + dependencies: + is-obj "^1.0.0" + +dotenv-expand@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" + integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== + +dotenv@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064" + integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w== + +dotenv@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + +downshift@^3.2.10: + version "3.2.10" + resolved "https://registry.yarnpkg.com/downshift/-/downshift-3.2.10.tgz#00f8e50383199c3f07ce63b575a840e2918b7b9f" + integrity sha512-fEYNbV/qDLUHTxF9wALNe51Xe5zauUhy2sqgYG1CtmAfUFMI30UuSaisU8CD0DEsFSIsaEvsVgtabb6nTEhtaA== + dependencies: + "@babel/runtime" "^7.1.2" + compute-scroll-into-view "^1.0.9" + prop-types "^15.6.0" + react-is "^16.5.2" + +duplexer@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= + +duplexify@^3.4.2, duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +electron-to-chromium@^1.3.191: + version "1.3.222" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.222.tgz#2a0e38903b2254d798dd8837507b5bc42c7e3934" + integrity sha512-Kv3rvtJELafNfgVBVNaDIdV0aWV7O1RlYqqAhg+s+OwpiXFYPsIvONYgAopmR/gpyxSYbHi0EKJmPOvaL7UzMg== + +electron-to-chromium@^1.3.247: + version "1.3.306" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.306.tgz#e8265301d053d5f74e36cb876486830261fbe946" + integrity sha512-frDqXvrIROoYvikSKTIKbHbzO6M3/qC6kCIt/1FOa9kALe++c4VAJnwjSFvf1tYLEUsP2n9XZ4XSCyqc3l7A/A== + +elegant-spinner@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" + integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= + +elliptic@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.0.tgz#2b8ed4c891b7de3200e14412a5b8248c7af505ca" + integrity sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg== + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emoji-regex@^7.0.1, emoji-regex@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= + dependencies: + iconv-lite "~0.4.13" + +end-of-stream@^1.0.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + dependencies: + once "^1.4.0" + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" + integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + tapable "^1.0.0" + +entities@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" + integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== + +env-ci@3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/env-ci/-/env-ci-3.2.2.tgz#06936f1fcfbc999102a2211fc2539df64062b61f" + integrity sha512-AOiNZ3lmxrtva3r/roqaYDF+1PX2V+ouUzuGqJf7KNxyyYkuU+CsfFbbUeibQPdixxjI/lP6eDtvtkX1/wymJw== + dependencies: + execa "^1.0.0" + java-properties "^1.0.0" + +enzyme-adapter-react-16@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz#a8f4278b47e082fbca14f5bfb1ee50ee650717b4" + integrity sha512-kC8pAtU2Jk3OJ0EG8Y2813dg9Ol0TXi7UNxHzHiWs30Jo/hj7alc//G1YpKUsPP1oKl9X+Lkx+WlGJpPYA+nvw== + dependencies: + enzyme-adapter-utils "^1.3.0" + lodash "^4.17.4" + object.assign "^4.0.4" + object.values "^1.0.4" + prop-types "^15.6.0" + react-reconciler "^0.7.0" + react-test-renderer "^16.0.0-0" + +enzyme-adapter-utils@^1.3.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.0.tgz#96e3730d76b872f593e54ce1c51fa3a451422d93" + integrity sha512-wkZvE0VxcFx/8ZsBw0iAbk3gR1d9hK447ebnSYBf95+r32ezBq+XDSAvRErkc4LZosgH8J7et7H7/7CtUuQfBA== + dependencies: + airbnb-prop-types "^2.13.2" + function.prototype.name "^1.1.0" + object.assign "^4.1.0" + object.fromentries "^2.0.0" + prop-types "^15.7.2" + semver "^5.6.0" + +errno@^0.1.3, errno@~0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== + dependencies: + prr "~1.0.1" + +error-ex@^1.2.0, error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.7.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" + integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-keys "^1.0.12" + +es-abstract@^1.13.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.15.0.tgz#8884928ec7e40a79e3c9bc812d37d10c8b24cc57" + integrity sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.0" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-inspect "^1.6.0" + object-keys "^1.1.1" + string.prototype.trimleft "^2.1.0" + string.prototype.trimright "^2.1.0" + +es-abstract@^1.5.1: + version "1.16.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d" + integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.0" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-inspect "^1.6.0" + object-keys "^1.1.1" + string.prototype.trimleft "^2.1.0" + string.prototype.trimright "^2.1.0" + +es-to-primitive@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14: + version "0.10.50" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.50.tgz#6d0e23a0abdb27018e5ac4fd09b412bc5517a778" + integrity sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw== + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.1" + next-tick "^1.0.0" + +es6-iterator@2.0.3, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= + dependencies: + d "1" + es5-ext "~0.10.14" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escodegen@^1.11.0, escodegen@^1.9.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" + integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +eslint-config-react-app@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-5.0.2.tgz#df40d73a1402986030680c040bbee520db5a32a4" + integrity sha512-VhlESAQM83uULJ9jsvcKxx2Ab0yrmjUt8kDz5DyhTQufqWE0ssAnejlWri5LXv25xoXfdqOyeDPdfJS9dXKagQ== + dependencies: + confusing-browser-globals "^1.0.9" + +eslint-import-resolver-node@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" + integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== + dependencies: + debug "^2.6.9" + resolve "^1.5.0" + +eslint-loader@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-3.0.2.tgz#5a627316a51d6f41d357b9f6f0554e91506cdd6e" + integrity sha512-S5VnD+UpVY1PyYRqeBd/4pgsmkvSokbHqTXAQMpvCyRr3XN2tvSLo9spm2nEpqQqh9dezw3os/0zWihLeOg2Rw== + dependencies: + fs-extra "^8.1.0" + loader-fs-cache "^1.0.2" + loader-utils "^1.2.3" + object-hash "^1.3.1" + schema-utils "^2.2.0" + +eslint-module-utils@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz#7b4675875bf96b0dbf1b21977456e5bb1f5e018c" + integrity sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw== + dependencies: + debug "^2.6.8" + pkg-dir "^2.0.0" + +eslint-plugin-flowtype@3.13.0: + version "3.13.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.13.0.tgz#e241ebd39c0ce519345a3f074ec1ebde4cf80f2c" + integrity sha512-bhewp36P+t7cEV0b6OdmoRWJCBYRiHFlqPZAG1oS3SF+Y0LQkeDvFSM4oxoxvczD1OdONCXMlJfQFiWLcV9urw== + dependencies: + lodash "^4.17.15" + +eslint-plugin-import@2.18.2: + version "2.18.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6" + integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ== + dependencies: + array-includes "^3.0.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.2" + eslint-module-utils "^2.4.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.0" + read-pkg-up "^2.0.0" + resolve "^1.11.0" + +eslint-plugin-jsx-a11y@6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz#b872a09d5de51af70a97db1eea7dc933043708aa" + integrity sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg== + dependencies: + "@babel/runtime" "^7.4.5" + aria-query "^3.0.0" + array-includes "^3.0.3" + ast-types-flow "^0.0.7" + axobject-query "^2.0.2" + damerau-levenshtein "^1.0.4" + emoji-regex "^7.0.2" + has "^1.0.3" + jsx-ast-utils "^2.2.1" + +eslint-plugin-react-hooks@^1.6.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz#6210b6d5a37205f0b92858f895a4e827020a7d04" + integrity sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA== + +eslint-plugin-react@7.14.3: + version "7.14.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz#911030dd7e98ba49e1b2208599571846a66bdf13" + integrity sha512-EzdyyBWC4Uz2hPYBiEJrKCUi2Fn+BJ9B/pJQcjw5X+x/H2Nm59S4MJIvL4O5NEE0+WbnQwEBxWY03oUk+Bc3FA== + dependencies: + array-includes "^3.0.3" + doctrine "^2.1.0" + has "^1.0.3" + jsx-ast-utils "^2.1.0" + object.entries "^1.1.0" + object.fromentries "^2.0.0" + object.values "^1.1.0" + prop-types "^15.7.2" + resolve "^1.10.1" + +eslint-scope@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-utils@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.0.tgz#e2c3c8dba768425f897cf0f9e51fe2e241485d4c" + integrity sha512-7ehnzPaP5IIEh1r1tkjuIrxqhNkzUJa9z3R92tLJdZIVdWaczEhr3EbhGtsMrVxi1KeR8qA7Off6SWc5WNQqyQ== + dependencies: + eslint-visitor-keys "^1.0.0" + +eslint-utils@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" + integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ== + +eslint-visitor-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" + integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + +eslint@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.1.0.tgz#06438a4a278b1d84fb107d24eaaa35471986e646" + integrity sha512-QhrbdRD7ofuV09IuE2ySWBz0FyXCq0rriLTZXZqaWSI79CVtHVRdkFuFTViiqzZhkCgfOh9USpriuGN2gIpZDQ== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.3.1" + eslint-visitor-keys "^1.0.0" + espree "^6.0.0" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^11.7.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^6.4.1" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.0.0.tgz#716fc1f5a245ef5b9a7fdb1d7b0d3f02322e75f6" + integrity sha512-lJvCS6YbCn3ImT3yKkPe0+tJ+mH6ljhGNjHQH9mRtiO6gjhVAOhVXW1yjnwqGwTkK3bGbye+hb00nFNmu0l/1Q== + dependencies: + acorn "^6.0.7" + acorn-jsx "^5.0.0" + eslint-visitor-keys "^1.0.0" + +esprima@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= + +esprima@^4.0.0, esprima@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= + +esutils@^2.0.0, esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +eventemitter3@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + +events@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" + integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== + +eventsource@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" + integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== + dependencies: + original "^1.0.0" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +exec-sh@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" + integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expect@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" + integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== + dependencies: + "@jest/types" "^24.9.0" + ansi-styles "^3.2.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.9.0" + +express@^4.16.2: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extract-stack@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/extract-stack/-/extract-stack-1.0.0.tgz#b97acaf9441eea2332529624b732fc5a1c8165fa" + integrity sha1-uXrK+UQe6iMyUpYktzL8WhyBZfo= + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-glob@^2.0.2: + version "2.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" + integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== + dependencies: + "@mrmlnc/readdir-enhanced" "^2.2.1" + "@nodelib/fs.stat" "^1.1.2" + glob-parent "^3.1.0" + is-glob "^4.0.0" + merge2 "^1.2.3" + micromatch "^3.1.10" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@~0.11.1: + version "0.11.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg= + dependencies: + bser "^2.0.0" + +fbjs@^0.8.16: + version "0.8.17" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" + integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.18" + +figgy-pudding@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" + integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== + +figures@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + +file-loader@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" + integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw== + dependencies: + loader-utils "^1.0.2" + schema-utils "^1.0.0" + +filesize@3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" + integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg== + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-cache-dir@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" + integrity sha1-yN765XyKUqinhPnjHFfHQumToLk= + dependencies: + commondir "^1.0.1" + mkdirp "^0.5.1" + pkg-dir "^1.0.0" + +find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-up@3.0.0, find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" + integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== + +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I= + +flush-write-stream@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" + integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== + dependencies: + inherits "^2.0.3" + readable-stream "^2.3.6" + +follow-redirects@^1.0.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" + integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== + dependencies: + debug "^3.2.6" + +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= + +for-in@^1.0.1, for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +for-own@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4= + dependencies: + for-in "^1.0.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +fork-ts-checker-webpack-plugin@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-1.5.0.tgz#ce1d77190b44d81a761b10b6284a373795e41f0c" + integrity sha512-zEhg7Hz+KhZlBhILYpXy+Beu96gwvkROWJiTXOCyOOMMrdBIRPvsBpBqgTI4jfJGrJXcqGwJR8zsBGDmzY0jsA== + dependencies: + babel-code-frame "^6.22.0" + chalk "^2.4.1" + chokidar "^2.0.4" + micromatch "^3.1.10" + minimatch "^3.0.4" + semver "^5.6.0" + tapable "^1.0.0" + worker-rpc "^0.1.0" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-extra@7.0.1, fs-extra@^7.0.0, fs-extra@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" + integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-6.0.1.tgz#8abc128f7946e310135ddc93b98bddb410e7a34b" + integrity sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-minipass@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" + integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== + dependencies: + minipass "^2.2.1" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a" + integrity sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ== + +fsevents@^1.2.7: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== + dependencies: + nan "^2.12.1" + node-pre-gyp "^0.12.0" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function.prototype.name@^1.1.0, function.prototype.name@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.1.tgz#6d252350803085abc2ad423d4fe3be2f9cbda392" + integrity sha512-e1NzkiJuw6xqVH7YSdiW/qDHebcmMhPNe6w+4ZYYEg0VA+LaLzx37RimbPLuonHhYGFGPx1ME2nSi74JiaCr/Q== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + functions-have-names "^1.1.1" + is-callable "^1.1.4" + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +functions-have-names@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.1.1.tgz#79d35927f07b8e7103d819fed475b64ccf7225ea" + integrity sha512-U0kNHUoxwPNPWOJaMG7Z00d4a/qZVrFtzWJRaK8V9goaVOCXBSQSJpt3MYGNtkScKEBKovxLjnNdC9MlXwo5Pw== + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" + integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== + dependencies: + globule "^1.0.0" + +get-best-contrast-color@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/get-best-contrast-color/-/get-best-contrast-color-0.2.2.tgz#8eaab0d21046d8acdac9057079422c59bb6f36aa" + integrity sha512-MbaqKhwqkL9xmklBfTwtip4TdHpbFfuYangE1gSFrdFje2ARKTVn8aFtE+hdQfB6jzg2TZ+NLo6cozy/cLrMiA== + dependencies: + get-contrast-ratio "0.1.4" + +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-contrast-ratio@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/get-contrast-ratio/-/get-contrast-ratio-0.1.4.tgz#a7a84a4beafd4011dbb1848f3d2e2d009ae76932" + integrity sha512-ARBdNfXyx5lgH3vq6/XNInR0s1SH9ZOfKah21c58qF4Hj4xwEyrxcSsaRZgv794ajS8KcfFQfqIwOZ2kwIrF2Q== + dependencies: + get-relative-luminance "0.1.2" + +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" + integrity sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg== + +get-relative-luminance@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/get-relative-luminance/-/get-relative-luminance-0.1.2.tgz#f0e77797fdd90d46e9a8a3a9531d97190d2f560c" + integrity sha1-8Od3l/3ZDUbpqKOpUx2XGQ0vVgw= + dependencies: + parse-color "1.0.0" + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +git-parse@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/git-parse/-/git-parse-1.0.3.tgz#82f165201892688ec9286184b3eee5c4cf0655ac" + integrity sha512-LlGDePBQ9Lr/jsL3ULrnV8SQL8sk3cdScyc+vAk6jVLkHBOxdIj3JosNWemH2o9pNnGtcqukl+ym1Nl6k5jw0Q== + dependencies: + babel-polyfill "6.26.0" + byline "5.0.0" + util.promisify "1.0.0" + +git-rev-sync@1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/git-rev-sync/-/git-rev-sync-1.12.0.tgz#4468406c7e6c3ba4cf4587999e1adb28d9d1af55" + integrity sha1-RGhAbH5sO6TPRYeZnhrbKNnRr1U= + dependencies: + escape-string-regexp "1.0.5" + graceful-fs "4.1.11" + shelljs "0.7.7" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954" + integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= + +glob@7.1.4, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.1.3, glob@^7.1.6, glob@~7.1.1: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0, globals@^11.7.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globby@8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d" + integrity sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w== + dependencies: + array-union "^1.0.1" + dir-glob "2.0.0" + fast-glob "^2.0.2" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globule@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d" + integrity sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ== + dependencies: + glob "~7.1.1" + lodash "~4.17.10" + minimatch "~3.0.2" + +graceful-fs@4.1.11: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= + +graceful-fs@^4.1.11, graceful-fs@^4.1.15: + version "4.2.1" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.1.tgz#1c1f0c364882c868f5bff6512146328336a11b1d" + integrity sha512-b9usnbDGnD928gJB3LrCmxoibr3VE4U2SMo5PBuBnokWyDADTqDPXg4YpwKF1trpH+UbGp7QLicO3+aWEy0+mw== + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + +graphql-tag@2.10.1, graphql-tag@^2.10.1, graphql-tag@^2.4.2: + version "2.10.1" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02" + integrity sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg== + +graphql@*, "graphql@14.0.2 - 14.2.0 || ^14.3.1", graphql@^14.5.8: + version "14.5.8" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.8.tgz#504f3d3114cb9a0a3f359bbbcf38d9e5bf6a6b3c" + integrity sha512-MMwmi0zlVLQKLdGiMfWkgQD7dY/TUKt4L+zgJ/aR0Howebod3aNgP5JkgvAULiR2HPVZaP2VEElqtdidHweLkg== + dependencies: + iterall "^1.2.2" + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + +gud@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" + integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== + +gzip-size@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" + integrity sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA== + dependencies: + duplexer "^0.1.1" + pify "^4.0.1" + +handle-thing@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" + integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== + +handlebars@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" + integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== + dependencies: + neo-async "^2.6.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +harmony-reflect@^1.4.6: + version "1.6.1" + resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.1.tgz#c108d4f2bb451efef7a37861fdbdae72c9bdefa9" + integrity sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA== + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.0, has@^1.0.1, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +he@1.2.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +header-case@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/header-case/-/header-case-1.0.1.tgz#9535973197c144b09613cd65d317ef19963bd02d" + integrity sha1-lTWXMZfBRLCWE81l0xfvGZY70C0= + dependencies: + no-case "^2.2.0" + upper-case "^1.1.3" + +hex-color-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" + integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== + +history@^4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" + integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^2.2.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^0.4.0" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" + integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== + dependencies: + react-is "^16.7.0" + +hoist-non-react-statics@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#101685d3aff3b23ea213163f6e8e12f4f111e19f" + integrity sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw== + dependencies: + react-is "^16.7.0" + +hosted-git-info@^2.1.4: + version "2.8.2" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.2.tgz#a35c3f355ac1249f1093c0c2a542ace8818c171a" + integrity sha512-CyjlXII6LMsPMyUzxpTt8fzh5QwzGqPmQXgY/Jyf4Zfp27t/FvfhwoE/8laaMUcMy816CkWF20I7NeQhwwY88w== + dependencies: + lru-cache "^5.1.1" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +hsl-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" + integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= + +hsla-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" + integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= + +html-comment-regex@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" + integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== + +html-encoding-sniffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" + integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== + dependencies: + whatwg-encoding "^1.0.1" + +html-entities@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= + +html-minifier@^3.5.20: + version "3.5.21" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c" + integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA== + dependencies: + camel-case "3.0.x" + clean-css "4.2.x" + commander "2.17.x" + he "1.2.x" + param-case "2.1.x" + relateurl "0.2.x" + uglify-js "3.4.x" + +html-webpack-plugin@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.5.tgz#2c53083c1151bfec20479b1f8aaf0039e77b5513" + integrity sha512-y5l4lGxOW3pz3xBTFdfB9rnnrWRPVxlAhX6nrBYIcW+2k2zC3mSp/3DxlWVCMBfnO6UAnoF8OcFn0IMy6kaKAQ== + dependencies: + html-minifier "^3.5.20" + loader-utils "^1.1.0" + lodash "^4.17.11" + pretty-error "^2.1.1" + tapable "^1.1.0" + util.promisify "1.0.0" + +htmlparser2@^3.3.0: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + +http-call@^5.2.2: + version "5.2.5" + resolved "https://registry.yarnpkg.com/http-call/-/http-call-5.2.5.tgz#cccb144230dd2f379cf61800fd4461e24571c1be" + integrity sha512-SfJ9j2xfi8zhQuJxcBCN1AhPCUAvPhipNaoeHWHfHiV0gz4uf9RUt2kl+xu9mxJLKxhNP7We87aRGbaSGPjr8A== + dependencies: + content-type "^1.0.4" + debug "^4.1.1" + is-retry-allowed "^1.1.0" + is-stream "^2.0.0" + parse-json "^4.0.0" + tunnel-agent "^0.6.0" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +"http-parser-js@>=0.4.0 <0.4.11": + version "0.4.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" + integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= + +http-proxy-middleware@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== + dependencies: + http-proxy "^1.17.0" + is-glob "^4.0.0" + lodash "^4.17.11" + micromatch "^3.1.10" + +http-proxy@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" + integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== + dependencies: + eventemitter3 "^3.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= + +hyperlinker@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" + integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ== + +hyphenate-style-name@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" + integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== + +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= + +icss-utils@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + +identity-obj-proxy@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" + integrity sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ= + dependencies: + harmony-reflect "^1.4.6" + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== + dependencies: + minimatch "^3.0.4" + +ignore@^3.3.5: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +immer@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" + integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== + +immutable@^3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" + integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM= + +import-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" + integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= + dependencies: + import-from "^2.1.0" + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-fresh@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.1.0.tgz#6d33fa1dcef6df930fae003446f33415af905118" + integrity sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-from@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" + integrity sha1-M1238qev/VOqpHHUuAId7ja387E= + dependencies: + resolve-from "^3.0.0" + +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^3.0.0, indent-string@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + +infer-owner@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflected@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inflected/-/inflected-2.0.4.tgz#323770961ccbe992a98ea930512e9a82d3d3ef77" + integrity sha512-HQPzFLTTUvwfeUH6RAGjD8cHS069mBqXG5n4qaxX7sJXBhVQrsGgF+0ZJGkSuN6a8pcUWB/GXStta11kKi/WvA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@^1.3.5, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +inquirer@6.5.0, inquirer@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.0.tgz#2303317efc9a4ea7ec2e2df6f86569b734accf42" + integrity sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA== + dependencies: + ansi-escapes "^3.2.0" + chalk "^2.4.2" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^2.0.0" + lodash "^4.17.12" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.4.0" + string-width "^2.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + +internal-ip@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" + integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== + dependencies: + default-gateway "^4.2.0" + ipaddr.js "^1.9.0" + +interpret@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" + integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== + +invariant@^2.2.2, invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= + +ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + +ipaddr.js@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" + integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== + +ipaddr.js@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.0.2, is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-color-stop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" + integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= + dependencies: + css-color-names "^0.0.4" + hex-color-regex "^1.1.0" + hsl-regex "^1.0.0" + hsla-regex "^1.0.0" + rgb-regex "^1.0.1" + rgba-regex "^1.0.0" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-in-browser@^1.0.2, is-in-browser@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" + integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= + +is-lower-case@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393" + integrity sha1-fhR75HaNxGbbO/shzGCzHmrWk5M= + dependencies: + lower-case "^1.1.0" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-obj@^1.0.0, is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-observable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" + integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA== + dependencies: + symbol-observable "^1.1.0" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0= + +is-path-in-cwd@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" + integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ== + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= + dependencies: + path-is-inside "^1.0.1" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= + dependencies: + has "^1.0.1" + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== + +is-retry-allowed@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" + integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== + +is-root@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" + integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== + +is-stream@^1.0.1, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-svg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" + integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== + dependencies: + html-comment-regex "^1.1.0" + +is-symbol@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + dependencies: + has-symbols "^1.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-upper-case@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-upper-case/-/is-upper-case-1.1.2.tgz#8d0b1fa7e7933a1e58483600ec7d9661cbaf756f" + integrity sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8= + dependencies: + upper-case "^1.1.0" + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk= + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" + integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== + dependencies: + "@babel/generator" "^7.4.0" + "@babel/parser" "^7.4.3" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.3" + "@babel/types" "^7.4.0" + istanbul-lib-coverage "^2.0.5" + semver "^6.0.0" + +istanbul-lib-report@^2.0.4: + version "2.0.8" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" + integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== + dependencies: + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + supports-color "^6.1.0" + +istanbul-lib-source-maps@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" + +istanbul-reports@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af" + integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== + dependencies: + handlebars "^4.1.2" + +iterall@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" + integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== + +java-properties@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/java-properties/-/java-properties-1.0.2.tgz#ccd1fa73907438a5b5c38982269d0e771fe78211" + integrity sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ== + +jest-changed-files@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" + integrity sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg== + dependencies: + "@jest/types" "^24.9.0" + execa "^1.0.0" + throat "^4.0.0" + +jest-cli@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af" + integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg== + dependencies: + "@jest/core" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + exit "^0.1.2" + import-local "^2.0.0" + is-ci "^2.0.0" + jest-config "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + prompts "^2.0.1" + realpath-native "^1.1.0" + yargs "^13.3.0" + +jest-config@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5" + integrity sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ== + dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^24.9.0" + "@jest/types" "^24.9.0" + babel-jest "^24.9.0" + chalk "^2.0.1" + glob "^7.1.1" + jest-environment-jsdom "^24.9.0" + jest-environment-node "^24.9.0" + jest-get-type "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + micromatch "^3.1.10" + pretty-format "^24.9.0" + realpath-native "^1.1.0" + +jest-diff@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" + integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== + dependencies: + chalk "^2.0.1" + diff-sequences "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-docblock@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.3.0.tgz#b9c32dac70f72e4464520d2ba4aec02ab14db5dd" + integrity sha512-nlANmF9Yq1dufhFlKG9rasfQlrY7wINJbo3q01tu56Jv5eBU5jirylhF2O5ZBnLxzOVBGRDz/9NAwNyBtG4Nyg== + dependencies: + detect-newline "^2.1.0" + +jest-each@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05" + integrity sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog== + dependencies: + "@jest/types" "^24.9.0" + chalk "^2.0.1" + jest-get-type "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" + +jest-environment-jsdom-fourteen@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom-fourteen/-/jest-environment-jsdom-fourteen-0.1.0.tgz#aad6393a9d4b565b69a609109bf469f62bf18ccc" + integrity sha512-4vtoRMg7jAstitRzL4nbw83VmGH8Rs13wrND3Ud2o1fczDhMUF32iIrNKwYGgeOPUdfvZU4oy8Bbv+ni1fgVCA== + dependencies: + jest-mock "^24.5.0" + jest-util "^24.5.0" + jsdom "^14.0.0" + +jest-environment-jsdom@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b" + integrity sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" + jsdom "^11.5.1" + +jest-environment-node@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3" + integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" + +jest-get-type@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" + integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== + +jest-haste-map@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" + integrity sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ== + dependencies: + "@jest/types" "^24.9.0" + anymatch "^2.0.0" + fb-watchman "^2.0.0" + graceful-fs "^4.1.15" + invariant "^2.2.4" + jest-serializer "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.9.0" + micromatch "^3.1.10" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^1.2.7" + +jest-jasmine2@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0" + integrity sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw== + dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + co "^4.6.0" + expect "^24.9.0" + is-generator-fn "^2.0.0" + jest-each "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" + throat "^4.0.0" + +jest-leak-detector@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a" + integrity sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA== + dependencies: + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-matcher-utils@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" + integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA== + dependencies: + chalk "^2.0.1" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-message-util@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.8.0.tgz#0d6891e72a4beacc0292b638685df42e28d6218b" + integrity sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" + "@types/stack-utils" "^1.0.1" + chalk "^2.0.1" + micromatch "^3.1.10" + slash "^2.0.0" + stack-utils "^1.0.1" + +jest-message-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" + integrity sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/stack-utils" "^1.0.1" + chalk "^2.0.1" + micromatch "^3.1.10" + slash "^2.0.0" + stack-utils "^1.0.1" + +jest-mock@^24.5.0, jest-mock@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.8.0.tgz#2f9d14d37699e863f1febf4e4d5a33b7fdbbde56" + integrity sha512-6kWugwjGjJw+ZkK4mDa0Df3sDlUTsV47MSrT0nGQ0RBWJbpODDQ8MHDVtGtUYBne3IwZUhtB7elxHspU79WH3A== + dependencies: + "@jest/types" "^24.8.0" + +jest-mock@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" + integrity sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w== + dependencies: + "@jest/types" "^24.9.0" + +jest-pnp-resolver@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" + integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== + +jest-regex-util@^24.3.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.3.0.tgz#d5a65f60be1ae3e310d5214a0307581995227b36" + integrity sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg== + +jest-regex-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" + integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA== + +jest-resolve-dependencies@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab" + integrity sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g== + dependencies: + "@jest/types" "^24.9.0" + jest-regex-util "^24.3.0" + jest-snapshot "^24.9.0" + +jest-resolve@24.9.0, jest-resolve@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" + integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== + dependencies: + "@jest/types" "^24.9.0" + browser-resolve "^1.11.3" + chalk "^2.0.1" + jest-pnp-resolver "^1.2.1" + realpath-native "^1.1.0" + +jest-runner@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42" + integrity sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.4.2" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-docblock "^24.3.0" + jest-haste-map "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-leak-detector "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + source-map-support "^0.5.6" + throat "^4.0.0" + +jest-runtime@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac" + integrity sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/source-map" "^24.3.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + realpath-native "^1.1.0" + slash "^2.0.0" + strip-bom "^3.0.0" + yargs "^13.3.0" + +jest-serializer@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73" + integrity sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ== + +jest-snapshot@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" + integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew== + dependencies: + "@babel/types" "^7.0.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + expect "^24.9.0" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + pretty-format "^24.9.0" + semver "^6.2.0" + +jest-util@^24.5.0, jest-util@^24.8.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.8.0.tgz#41f0e945da11df44cc76d64ffb915d0716f46cd1" + integrity sha512-DYZeE+XyAnbNt0BG1OQqKy/4GVLPtzwGx5tsnDrFcax36rVE3lTA5fbvgmbVPUZf9w77AJ8otqR4VBbfFJkUZA== + dependencies: + "@jest/console" "^24.7.1" + "@jest/fake-timers" "^24.8.0" + "@jest/source-map" "^24.3.0" + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" + callsites "^3.0.0" + chalk "^2.0.1" + graceful-fs "^4.1.15" + is-ci "^2.0.0" + mkdirp "^0.5.1" + slash "^2.0.0" + source-map "^0.6.0" + +jest-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.9.0.tgz#7396814e48536d2e85a37de3e4c431d7cb140162" + integrity sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg== + dependencies: + "@jest/console" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/source-map" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + callsites "^3.0.0" + chalk "^2.0.1" + graceful-fs "^4.1.15" + is-ci "^2.0.0" + mkdirp "^0.5.1" + slash "^2.0.0" + source-map "^0.6.0" + +jest-validate@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" + integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ== + dependencies: + "@jest/types" "^24.9.0" + camelcase "^5.3.1" + chalk "^2.0.1" + jest-get-type "^24.9.0" + leven "^3.1.0" + pretty-format "^24.9.0" + +jest-watch-typeahead@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-0.4.0.tgz#4d5356839a85421588ce452d2440bf0d25308397" + integrity sha512-bJR/HPNgOQnkmttg1OkBIrYFAYuxFxExtgQh67N2qPvaWGVC8TCkedRNPKBfmZfVXFD3u2sCH+9OuS5ApBfCgA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^2.4.1" + jest-watcher "^24.3.0" + slash "^3.0.0" + string-length "^3.1.0" + strip-ansi "^5.0.0" + +jest-watcher@^24.3.0: + version "24.8.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.8.0.tgz#58d49915ceddd2de85e238f6213cef1c93715de4" + integrity sha512-SBjwHt5NedQoVu54M5GEx7cl7IGEFFznvd/HNT8ier7cCAx/Qgu9ZMlaTQkvK22G1YOpcWBLQPFSImmxdn3DAw== + dependencies: + "@jest/test-result" "^24.8.0" + "@jest/types" "^24.8.0" + "@types/yargs" "^12.0.9" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + jest-util "^24.8.0" + string-length "^2.0.0" + +jest-watcher@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b" + integrity sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw== + dependencies: + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + jest-util "^24.9.0" + string-length "^2.0.0" + +jest-worker@^24.6.0: + version "24.6.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.6.0.tgz#7f81ceae34b7cde0c9827a6980c35b7cdc0161b3" + integrity sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ== + dependencies: + merge-stream "^1.0.1" + supports-color "^6.1.0" + +jest-worker@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" + integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== + dependencies: + merge-stream "^2.0.0" + supports-color "^6.1.0" + +jest@24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171" + integrity sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw== + dependencies: + import-local "^2.0.0" + jest-cli "^24.9.0" + +js-levenshtein@^1.1.3: + version "1.1.6" + resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" + integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsdom@^11.5.1: + version "11.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" + integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== + dependencies: + abab "^2.0.0" + acorn "^5.5.3" + acorn-globals "^4.1.0" + array-equal "^1.0.0" + cssom ">= 0.3.2 < 0.4.0" + cssstyle "^1.0.0" + data-urls "^1.0.0" + domexception "^1.0.1" + escodegen "^1.9.1" + html-encoding-sniffer "^1.0.2" + left-pad "^1.3.0" + nwsapi "^2.0.7" + parse5 "4.0.0" + pn "^1.1.0" + request "^2.87.0" + request-promise-native "^1.0.5" + sax "^1.2.4" + symbol-tree "^3.2.2" + tough-cookie "^2.3.4" + w3c-hr-time "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-encoding "^1.0.3" + whatwg-mimetype "^2.1.0" + whatwg-url "^6.4.1" + ws "^5.2.0" + xml-name-validator "^3.0.0" + +jsdom@^14.0.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-14.1.0.tgz#916463b6094956b0a6c1782c94e380cd30e1981b" + integrity sha512-O901mfJSuTdwU2w3Sn+74T+RnDVP+FuV5fH8tcPWyqrseRAb0s5xOtPgCFiPOtLcyK7CLIJwPyD83ZqQWvA5ng== + dependencies: + abab "^2.0.0" + acorn "^6.0.4" + acorn-globals "^4.3.0" + array-equal "^1.0.0" + cssom "^0.3.4" + cssstyle "^1.1.1" + data-urls "^1.1.0" + domexception "^1.0.1" + escodegen "^1.11.0" + html-encoding-sniffer "^1.0.2" + nwsapi "^2.1.3" + parse5 "5.1.0" + pn "^1.1.0" + request "^2.88.0" + request-promise-native "^1.0.5" + saxes "^3.1.9" + symbol-tree "^3.2.2" + tough-cookie "^2.5.0" + w3c-hr-time "^1.0.1" + w3c-xmlserializer "^1.1.2" + webidl-conversions "^4.0.2" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^7.0.0" + ws "^6.1.2" + xml-name-validator "^3.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= + +json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json3@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" + integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +json5@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" + integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== + dependencies: + minimist "^1.2.0" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jss-plugin-camel-case@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.0.0.tgz#d601bae2e8e2041cc526add289dcd7062db0a248" + integrity sha512-yALDL00+pPR4FJh+k07A8FeDvfoPPuXU48HLy63enAubcVd3DnS+2rgqPXglHDGixIDVkCSXecl/l5GAMjzIbA== + dependencies: + "@babel/runtime" "^7.3.1" + hyphenate-style-name "^1.0.3" + jss "10.0.0" + +jss-plugin-default-unit@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.0.0.tgz#601caf5f576fc0c66986fbe8a9aa37307a3a3ea3" + integrity sha512-sURozIOdCtGg9ap18erQ+ijndAfEGtTaetxfU3H4qwC18Bi+fdvjlY/ahKbuu0ASs7R/+WKCP7UaRZOjUDMcdQ== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.0.0" + +jss-plugin-global@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.0.0.tgz#0fed1b6461e0d57d6e394f877529009bc1cb3cb6" + integrity sha512-80ofWKSQUo62bxLtRoTNe0kFPtHgUbAJeOeR36WEGgWIBEsXLyXOnD5KNnjPqG4heuEkz9eSLccjYST50JnI7Q== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.0.0" + +jss-plugin-nested@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.0.0.tgz#d37ecc013c3b0d0e4acc2b48f6b62da6ae53948b" + integrity sha512-waxxwl/po1hN3azTyixKnr8ReEqUv5WK7WsO+5AWB0bFndML5Yqnt8ARZ90HEg8/P6WlqE/AB2413TkCRZE8bA== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.0.0" + tiny-warning "^1.0.2" + +jss-plugin-props-sort@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.0.0.tgz#38a13407384c2a4a7c026659488350669b953b18" + integrity sha512-41mf22CImjwNdtOG3r+cdC8+RhwNm616sjHx5YlqTwtSJLyLFinbQC/a4PIFk8xqf1qpFH1kEAIw+yx9HaqZ3g== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.0.0" + +jss-plugin-rule-value-function@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.0.0.tgz#3ec1b781b7c86080136dbef6c36e91f20244b72e" + integrity sha512-Jw+BZ8JIw1f12V0SERqGlBT1JEPWax3vuZpMym54NAXpPb7R1LYHiCTIlaJUyqvIfEy3kiHMtgI+r2whGgRIxQ== + dependencies: + "@babel/runtime" "^7.3.1" + jss "10.0.0" + +jss-plugin-vendor-prefixer@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.0.0.tgz#400280535b0f483a9c78105afe4eee61b70018eb" + integrity sha512-qslqvL0MUbWuzXJWdUxpj6mdNUX8jr4FFTo3aZnAT65nmzWL7g8oTr9ZxmTXXgdp7ANhS1QWE7036/Q2isFBpw== + dependencies: + "@babel/runtime" "^7.3.1" + css-vendor "^2.0.6" + jss "10.0.0" + +jss@10.0.0, jss@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/jss/-/jss-10.0.0.tgz#998d5026c02accae15708de83bd6ba57bac977d2" + integrity sha512-TPpDFsiBjuERiL+dFDq8QCdiF9oDasPcNqCKLGCo/qED3fNYOQ8PX2lZhknyTiAt3tZrfOFbb0lbQ9lTjPZxsQ== + dependencies: + "@babel/runtime" "^7.3.1" + csstype "^2.6.5" + is-in-browser "^1.1.3" + tiny-warning "^1.0.2" + +jsx-ast-utils@^2.1.0, jsx-ast-utils@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz#4d4973ebf8b9d2837ee91a8208cc66f3a2776cfb" + integrity sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ== + dependencies: + array-includes "^3.0.3" + object.assign "^4.1.0" + +killable@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== + +kind-of@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" + integrity sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU= + dependencies: + is-buffer "^1.0.2" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +last-call-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" + integrity sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w== + dependencies: + lodash "^4.17.5" + webpack-sources "^1.1.0" + +lazy-cache@^0.2.3: + version "0.2.7" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" + integrity sha1-f+3fLctu23fRHvHRF6tf/fCrG2U= + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= + +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== + dependencies: + invert-kv "^2.0.0" + +left-pad@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" + integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +listr-silent-renderer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" + integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= + +listr-update-renderer@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2" + integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA== + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + elegant-spinner "^1.0.1" + figures "^1.7.0" + indent-string "^3.0.0" + log-symbols "^1.0.2" + log-update "^2.3.0" + strip-ansi "^3.0.1" + +listr-verbose-renderer@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db" + integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw== + dependencies: + chalk "^2.4.1" + cli-cursor "^2.1.0" + date-fns "^1.27.2" + figures "^2.0.0" + +listr@0.14.3: + version "0.14.3" + resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" + integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== + dependencies: + "@samverschueren/stream-to-observable" "^0.3.0" + is-observable "^1.1.0" + is-promise "^2.1.0" + is-stream "^1.1.0" + listr-silent-renderer "^1.1.1" + listr-update-renderer "^0.5.0" + listr-verbose-renderer "^0.5.0" + p-map "^2.0.0" + rxjs "^6.3.3" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +load-json-file@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-5.3.0.tgz#4d3c1e01fa1c03ea78a60ac7af932c9ce53403f3" + integrity sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw== + dependencies: + graceful-fs "^4.1.15" + parse-json "^4.0.0" + pify "^4.0.1" + strip-bom "^3.0.0" + type-fest "^0.3.0" + +loader-fs-cache@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.2.tgz#54cedf6b727e1779fd8f01205f05f6e88706f086" + integrity sha512-70IzT/0/L+M20jUlEqZhZyArTU6VKLRTYRDAYN26g4jfzpJqjipLL3/hgYpySqI9PwsVRHHFja0LfEmsx9X2Cw== + dependencies: + find-cache-dir "^0.1.1" + mkdirp "0.5.1" + +loader-runner@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" + integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== + +loader-utils@1.2.3, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== + dependencies: + big.js "^5.2.2" + emojis-list "^2.0.0" + json5 "^1.0.1" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + +lodash.get@^4: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.identity@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash.identity/-/lodash.identity-3.0.0.tgz#ad7bc6a4e647d79c972e1b80feef7af156267876" + integrity sha1-rXvGpOZH15yXLhuA/u968VYmeHY= + +lodash.isarray@3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U= + +lodash.isequal@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + +lodash.isfinite@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.2.0.tgz#aa69ffb93a37e82fab0ce18862655f9174ced339" + integrity sha1-qmn/uTo36C+rDOGIYmVfkXTO0zk= + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.merge@^4.6.1: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.pickby@4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" + integrity sha1-feoh2MGNdwOifHBMFdO4SmfjOv8= + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash.template@^4.4.0, lodash.template@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= + +lodash.unescape@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" + integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +lodash.xorby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.xorby/-/lodash.xorby-4.7.0.tgz#9c19a6f9f063a6eb53dd03c1b6871799801463d7" + integrity sha1-nBmm+fBjputT3QPBtocXmYAUY9c= + +"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +lodash@^4.0.1, lodash@~4.17.4: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +log-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" + integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= + dependencies: + chalk "^1.0.0" + +log-update@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" + integrity sha1-iDKP19HOeTiykoN0bwsbwSayRwg= + dependencies: + ansi-escapes "^3.0.0" + cli-cursor "^2.0.0" + wrap-ansi "^3.0.1" + +loglevel@^1.4.1: + version "1.6.3" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.3.tgz#77f2eb64be55a404c9fd04ad16d57c1d6d6b1280" + integrity sha512-LoEDv5pgpvWgPF4kNYuIp0qqSJVWak/dML0RY74xlzMZiT9w77teNAwKYKWBTYjlokMirg+o3jBwp+vlLrcfAA== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lower-case-first@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-1.0.2.tgz#e5da7c26f29a7073be02d52bac9980e5922adfa1" + integrity sha1-5dp8JvKacHO+AtUrrJmA5ZIq36E= + dependencies: + lower-case "^1.1.2" + +lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= + +lru-cache@^5.0.0, lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +make-dir@^2.0.0, make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-error@^1, make-error@^1.1.1: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= + dependencies: + tmpl "1.0.x" + +mamacro@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" + integrity sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA== + +map-age-cleaner@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +material-colors@^1.2.1: + version "1.2.6" + resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" + integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== + +math-expression-evaluator@^1.2.14: + version "1.2.17" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" + integrity sha1-3oGf282E3M2PrlnGrreWFbnSZqw= + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + +mdn-data@~1.1.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" + integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +mem@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^2.0.0" + p-is-promise "^2.0.0" + +memory-fs@^0.4.0, memory-fs@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +merge-deep@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2" + integrity sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA== + dependencies: + arr-union "^3.1.0" + clone-deep "^0.2.4" + kind-of "^3.0.2" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" + integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE= + dependencies: + readable-stream "^2.0.1" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.4.tgz#c9269589e6885a60cf80605d9522d4b67ca646e3" + integrity sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +microevent.ts@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" + integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== + +micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + dependencies: + mime-db "1.40.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.4.2, mime@^2.4.4: + version "2.4.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" + integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + +mimic-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mini-create-react-context@^0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189" + integrity sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw== + dependencies: + "@babel/runtime" "^7.4.0" + gud "^1.0.0" + tiny-warning "^1.0.2" + +mini-css-extract-plugin@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1" + integrity sha512-MNpRGbNA52q6U92i0qbVpQNsgk7LExy41MdAlG84FeytfDOtRIf/mCHdEgG8rpTKOaNKiqUnZdlptF469hxqOw== + dependencies: + loader-utils "^1.1.0" + normalize-url "1.9.1" + schema-utils "^1.0.0" + webpack-sources "^1.1.0" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= + +minimatch@3.0.4, minimatch@^3.0.4, minimatch@~3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.1.1, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + +minipass@^2.2.1, minipass@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" + integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" + integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== + dependencies: + minipass "^2.2.1" + +mississippi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" + integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^3.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + +mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +moment-timezone@^0.5.45: + version "0.5.45" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.45.tgz#cb685acd56bac10e69d93c536366eb65aa6bcf5c" + integrity sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ== + dependencies: + moment "^2.29.4" + +moment@2.24.0, moment@>=2.14.0, moment@^2.22.1, moment@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + +moment@^2.29.4, moment@^2.30.1: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= + +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +natural-orderby@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-2.0.3.tgz#8623bc518ba162f8ff1cdb8941d74deb0fdcc016" + integrity sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q== + +needle@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" + integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" + integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== + +next-tick@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +no-case@^2.2.0, no-case@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" + integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== + dependencies: + lower-case "^1.1.1" + +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + +node-fetch@^2.1.2, node-fetch@^2.2.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + +node-forge@0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df" + integrity sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^3.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.1" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.11.0" + vm-browserify "^1.0.1" + +node-modules-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" + integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= + +node-notifier@^5.4.2: + version "5.4.3" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50" + integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q== + dependencies: + growly "^1.3.0" + is-wsl "^1.1.0" + semver "^5.5.0" + shellwords "^0.1.1" + which "^1.3.0" + +node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +node-releases@^1.1.25: + version "1.1.26" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.26.tgz#f30563edc5c7dc20cf524cc8652ffa7be0762937" + integrity sha512-fZPsuhhUHMTlfkhDLGtfY80DSJTjOcx+qD1j5pqPkuhUHVS7xHZIg9EE4DHK8O3f0zTxXHX5VIkDG8pu98/wfQ== + dependencies: + semver "^5.3.0" + +node-releases@^1.1.29: + version "1.1.40" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.40.tgz#a94facfa8e2d612302601ca1361741d529c4515a" + integrity sha512-r4LPcC5b/bS8BdtWH1fbeK88ib/wg9aqmg6/s3ngNLn2Ewkn/8J6Iw3P9RTlfIAdSdvYvQl2thCY5Y+qTAQ2iQ== + dependencies: + semver "^6.3.0" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +normalize-scroll-left@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/normalize-scroll-left/-/normalize-scroll-left-0.2.0.tgz#9445d74275f303cc661e113329aefa492f58114c" + integrity sha512-t5oCENZJl8TGusJKoCJm7+asaSsPuNmK6+iEjrZ5TyBj2f02brCRsd4c83hwtu+e5d4LCSBZ0uoDlMjBo+A8yA== + +normalize-url@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +normalize-url@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" + integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== + +notistack@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/notistack/-/notistack-0.6.1.tgz#d29a7b85deb67ed144e5427438280961615c7b1b" + integrity sha512-tuXmw/0TBmdeHqRjWQwf9Jh298OMEwi18mrnTwxV4IfkxO0n2AplTj/WM9D9kV7aEmMjHTcHixRzllZUou1mAw== + dependencies: + classnames "^2.2.6" + prop-types "^15.6.2" + +npm-bundled@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" + integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== + +npm-packlist@^1.1.6: + version "1.4.4" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" + integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +npm-run-path@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" + integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== + dependencies: + path-key "^3.0.0" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +nth-check@^1.0.2, nth-check@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +nwsapi@^2.0.7, nwsapi@^2.1.3: + version "2.1.4" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.4.tgz#e006a878db23636f8e8a67d33ca0e4edf61a842f" + integrity sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.0.1.tgz#99504456c3598b5cad4fc59c26e8a9bb107fe0bd" + integrity sha1-mVBEVsNZi1ytT8WcJuipuxB/4L0= + +object-assign@4.1.1, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-hash@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" + integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== + +object-inspect@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" + integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== + +object-is@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" + integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY= + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-path@0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949" + integrity sha1-NwrnUvvzfePqcKhhwju6iRVpGUk= + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.assign@^4.0.4, object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.entries@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519" + integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has "^1.0.3" + +object.fromentries@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab" + integrity sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA== + dependencies: + define-properties "^1.1.2" + es-abstract "^1.11.0" + function-bind "^1.1.1" + has "^1.0.1" + +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +object.values@^1.0.4, object.values@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" + integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has "^1.0.3" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= + dependencies: + mimic-fn "^1.0.0" + +open@^6.3.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" + integrity sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg== + dependencies: + is-wsl "^1.1.0" + +opn@^5.1.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + +optimism@^0.10.0: + version "0.10.3" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.10.3.tgz#163268fdc741dea2fb50f300bedda80356445fd7" + integrity sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw== + dependencies: + "@wry/context" "^0.4.0" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optimize-css-assets-webpack-plugin@5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz#e2f1d4d94ad8c0af8967ebd7cf138dcb1ef14572" + integrity sha512-q9fbvCRS6EYtUKKSwI87qm2IxlyJK5b4dygW1rKUBT6mMDhdG5e5bZT63v6tnJR9F9FB/H5a0HTmtw+laUBxKA== + dependencies: + cssnano "^4.1.10" + last-call-webpack-plugin "^3.0.0" + +optionator@^0.8.1, optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +original@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== + dependencies: + url-parse "^1.4.3" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-locale@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" + integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== + dependencies: + execa "^1.0.0" + lcid "^2.0.0" + mem "^4.0.0" + +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + +p-each-series@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" + integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E= + dependencies: + p-reduce "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-is-promise@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== + dependencies: + p-try "^2.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-reduce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" + integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +pako@~1.0.5: + version "1.0.10" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" + integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== + +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + integrity sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY= + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + +param-case@2.1.x, param-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" + integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= + dependencies: + no-case "^2.2.0" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-asn1@^5.0.0: + version "5.1.4" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" + integrity sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw== + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +parse-color@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-color/-/parse-color-1.0.0.tgz#7b748b95a83f03f16a94f535e52d7f3d94658619" + integrity sha1-e3SLlag/A/FqlPU15S1/PZRlhhk= + dependencies: + color-convert "~0.5.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-ms@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" + integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== + +parse5@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" + integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== + +parse5@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" + integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascal-case@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-2.0.1.tgz#2d578d3455f660da65eca18ef95b4e0de912761e" + integrity sha1-LVeNNFX2YNpl7KGO+VtODekSdh4= + dependencies: + camel-case "^3.0.0" + upper-case-first "^1.1.0" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +password-prompt@^1.0.7, password-prompt@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/password-prompt/-/password-prompt-1.1.2.tgz#85b2f93896c5bd9e9f2d6ff0627fa5af3dc00923" + integrity sha512-bpuBhROdrhuN3E7G/koAju0WjVw9/uQOG5Co5mokNj0MiOSBVZS1JTwM4zl55hu0WFmIEFvO9cU9sJQiBIYeIA== + dependencies: + ansi-escapes "^3.1.0" + cross-spawn "^6.0.5" + +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== + +path-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/path-case/-/path-case-2.1.1.tgz#94b8037c372d3fe2906e465bb45e25d226e8eea5" + integrity sha1-lLgDfDctP+KQbkZbtF4l0ibo7qU= + dependencies: + no-case "^2.2.0" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-key@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.0.tgz#99a10d870a803bdd5ee6f0470e58dfcd2f9a54d3" + integrity sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg== + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + integrity sha1-Wf3g9DW62suhA6hOnTvGTpa5k30= + dependencies: + isarray "0.0.1" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= + dependencies: + pify "^2.0.0" + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +pbkdf2@^3.0.3: + version "3.0.17" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" + integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pirates@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== + dependencies: + node-modules-regexp "^1.0.0" + +pkg-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" + integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q= + dependencies: + find-up "^1.0.0" + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= + dependencies: + find-up "^2.1.0" + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-up@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" + integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= + dependencies: + find-up "^2.1.0" + +pn@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" + integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== + +pnp-webpack-plugin@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.5.0.tgz#62a1cd3068f46d564bb33c56eb250e4d586676eb" + integrity sha512-jd9olUr9D7do+RN8Wspzhpxhgp1n6Vd0NtQ4SFkmIACZoEL1nkyAdW9Ygrinjec0vgDcWjscFQQ1gDW8rsfKTg== + dependencies: + ts-pnp "^1.1.2" + +popper.js@^1.14.1: + version "1.14.7" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e" + integrity sha512-4q1hNvoUre/8srWsH7hnoSJ5xVmIL4qgz+s4qf2TnJIMyZFUFMGH+9vE7mXynAlHSZ/NdTmmow86muD0myUkVQ== + +portfinder@^1.0.9: + version "1.0.21" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.21.tgz#60e1397b95ac170749db70034ece306b9a27e324" + integrity sha512-ESabpDCzmBS3ekHbmpAIiESq3udRsCBGiBZLsC+HgBKv2ezb0R4oG+7RnYEVZ/ZCfhel5Tx3UzdNWA0Lox2QCA== + dependencies: + async "^1.5.2" + debug "^2.2.0" + mkdirp "0.5.x" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +postcss-attribute-case-insensitive@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.1.tgz#b2a721a0d279c2f9103a36331c88981526428cc7" + integrity sha512-L2YKB3vF4PetdTIthQVeT+7YiSzMoNMLLYxPXXppOOP7NoazEAy45sh2LvJ8leCQjfBcfkYQs8TtCcQjeZTp8A== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0" + +postcss-browser-comments@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-browser-comments/-/postcss-browser-comments-2.0.0.tgz#dc48d6a8ddbff188a80a000b7393436cb18aed88" + integrity sha512-xGG0UvoxwBc4Yx4JX3gc0RuDl1kc4bVihCzzk6UC72YPfq5fu3c717Nu8Un3nvnq1BJ31gBnFXIG/OaUTnpHgA== + dependencies: + postcss "^7.0.2" + +postcss-calc@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.1.tgz#36d77bab023b0ecbb9789d84dcb23c4941145436" + integrity sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ== + dependencies: + css-unit-converter "^1.1.1" + postcss "^7.0.5" + postcss-selector-parser "^5.0.0-rc.4" + postcss-value-parser "^3.3.1" + +postcss-color-functional-notation@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0" + integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-color-gray@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547" + integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.5" + postcss-values-parser "^2.0.0" + +postcss-color-hex-alpha@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz#a8d9ca4c39d497c9661e374b9c51899ef0f87388" + integrity sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw== + dependencies: + postcss "^7.0.14" + postcss-values-parser "^2.0.1" + +postcss-color-mod-function@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d" + integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-color-rebeccapurple@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77" + integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-colormin@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" + integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== + dependencies: + browserslist "^4.0.0" + color "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-convert-values@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" + integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-custom-media@^7.0.8: + version "7.0.8" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" + integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== + dependencies: + postcss "^7.0.14" + +postcss-custom-properties@^8.0.11: + version "8.0.11" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz#2d61772d6e92f22f5e0d52602df8fae46fa30d97" + integrity sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA== + dependencies: + postcss "^7.0.17" + postcss-values-parser "^2.0.1" + +postcss-custom-selectors@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba" + integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-dir-pseudo-class@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2" + integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-discard-comments@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" + integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== + dependencies: + postcss "^7.0.0" + +postcss-discard-duplicates@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" + integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== + dependencies: + postcss "^7.0.0" + +postcss-discard-empty@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" + integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== + dependencies: + postcss "^7.0.0" + +postcss-discard-overridden@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" + integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== + dependencies: + postcss "^7.0.0" + +postcss-double-position-gradients@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e" + integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA== + dependencies: + postcss "^7.0.5" + postcss-values-parser "^2.0.0" + +postcss-env-function@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7" + integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-flexbugs-fixes@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.1.0.tgz#e094a9df1783e2200b7b19f875dcad3b3aff8b20" + integrity sha512-jr1LHxQvStNNAHlgco6PzY308zvLklh7SJVYuWUwyUQncofaAlD2l+P/gxKHOdqWKe7xJSkVLFF/2Tp+JqMSZA== + dependencies: + postcss "^7.0.0" + +postcss-focus-visible@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" + integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g== + dependencies: + postcss "^7.0.2" + +postcss-focus-within@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680" + integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w== + dependencies: + postcss "^7.0.2" + +postcss-font-variant@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.0.tgz#71dd3c6c10a0d846c5eda07803439617bbbabacc" + integrity sha512-M8BFYKOvCrI2aITzDad7kWuXXTm0YhGdP9Q8HanmN4EF1Hmcgs1KK5rSHylt/lUJe8yLxiSwWAHdScoEiIxztg== + dependencies: + postcss "^7.0.2" + +postcss-gap-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715" + integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg== + dependencies: + postcss "^7.0.2" + +postcss-image-set-function@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" + integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-initial@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.1.tgz#99d319669a13d6c06ef8e70d852f68cb1b399b61" + integrity sha512-I2Sz83ZSHybMNh02xQDK609lZ1/QOyYeuizCjzEhlMgeV/HcDJapQiH4yTqLjZss0X6/6VvKFXUeObaHpJoINw== + dependencies: + lodash.template "^4.5.0" + postcss "^7.0.2" + +postcss-lab-function@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e" + integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-load-config@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.0.tgz#c84d692b7bb7b41ddced94ee62e8ab31b417b003" + integrity sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q== + dependencies: + cosmiconfig "^5.0.0" + import-cwd "^2.0.0" + +postcss-loader@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" + integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== + dependencies: + loader-utils "^1.1.0" + postcss "^7.0.0" + postcss-load-config "^2.0.0" + schema-utils "^1.0.0" + +postcss-logical@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5" + integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA== + dependencies: + postcss "^7.0.2" + +postcss-media-minmax@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" + integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== + dependencies: + postcss "^7.0.2" + +postcss-merge-longhand@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" + integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== + dependencies: + css-color-names "0.0.4" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + stylehacks "^4.0.0" + +postcss-merge-rules@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" + integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + cssnano-util-same-parent "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + vendors "^1.0.0" + +postcss-minify-font-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" + integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-gradients@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" + integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + is-color-stop "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-params@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" + integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== + dependencies: + alphanum-sort "^1.0.0" + browserslist "^4.0.0" + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + uniqs "^2.0.0" + +postcss-minify-selectors@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" + integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== + dependencies: + alphanum-sort "^1.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + +postcss-modules-local-by-default@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63" + integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + postcss-value-parser "^3.3.1" + +postcss-modules-scope@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz#ad3f5bf7856114f6fcab901b0502e2a2bc39d4eb" + integrity sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + +postcss-modules-values@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64" + integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w== + dependencies: + icss-replace-symbols "^1.1.0" + postcss "^7.0.6" + +postcss-nesting@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.1.tgz#b50ad7b7f0173e5b5e3880c3501344703e04c052" + integrity sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg== + dependencies: + postcss "^7.0.2" + +postcss-normalize-charset@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" + integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== + dependencies: + postcss "^7.0.0" + +postcss-normalize-display-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" + integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-positions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" + integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== + dependencies: + cssnano-util-get-arguments "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-repeat-style@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" + integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-string@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" + integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== + dependencies: + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-timing-functions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" + integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-unicode@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" + integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-url@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" + integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-whitespace@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" + integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize/-/postcss-normalize-7.0.1.tgz#eb51568d962b8aa61a8318383c8bb7e54332282e" + integrity sha512-NOp1fwrG+6kVXWo7P9SizCHX6QvioxFD/hZcI2MLxPmVnFJFC0j0DDpIuNw2tUDeCFMni59gCVgeJ1/hYhj2OQ== + dependencies: + "@csstools/normalize.css" "^9.0.1" + browserslist "^4.1.1" + postcss "^7.0.2" + postcss-browser-comments "^2.0.0" + +postcss-ordered-values@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" + integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== + dependencies: + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-overflow-shorthand@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30" + integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g== + dependencies: + postcss "^7.0.2" + +postcss-page-break@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf" + integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ== + dependencies: + postcss "^7.0.2" + +postcss-place@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62" + integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-preset-env@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz#c34ddacf8f902383b35ad1e030f178f4cdf118a5" + integrity sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg== + dependencies: + autoprefixer "^9.6.1" + browserslist "^4.6.4" + caniuse-lite "^1.0.30000981" + css-blank-pseudo "^0.1.4" + css-has-pseudo "^0.10.0" + css-prefers-color-scheme "^3.1.1" + cssdb "^4.4.0" + postcss "^7.0.17" + postcss-attribute-case-insensitive "^4.0.1" + postcss-color-functional-notation "^2.0.1" + postcss-color-gray "^5.0.0" + postcss-color-hex-alpha "^5.0.3" + postcss-color-mod-function "^3.0.3" + postcss-color-rebeccapurple "^4.0.1" + postcss-custom-media "^7.0.8" + postcss-custom-properties "^8.0.11" + postcss-custom-selectors "^5.1.2" + postcss-dir-pseudo-class "^5.0.0" + postcss-double-position-gradients "^1.0.0" + postcss-env-function "^2.0.2" + postcss-focus-visible "^4.0.0" + postcss-focus-within "^3.0.0" + postcss-font-variant "^4.0.0" + postcss-gap-properties "^2.0.0" + postcss-image-set-function "^3.0.1" + postcss-initial "^3.0.0" + postcss-lab-function "^2.0.1" + postcss-logical "^3.0.0" + postcss-media-minmax "^4.0.0" + postcss-nesting "^7.0.0" + postcss-overflow-shorthand "^2.0.0" + postcss-page-break "^2.0.0" + postcss-place "^4.0.1" + postcss-pseudo-class-any-link "^6.0.0" + postcss-replace-overflow-wrap "^3.0.0" + postcss-selector-matches "^4.0.0" + postcss-selector-not "^4.0.0" + +postcss-pseudo-class-any-link@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1" + integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-reduce-initial@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" + integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + +postcss-reduce-transforms@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" + integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== + dependencies: + cssnano-util-get-match "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-replace-overflow-wrap@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c" + integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw== + dependencies: + postcss "^7.0.2" + +postcss-safe-parser@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.1.tgz#8756d9e4c36fdce2c72b091bbc8ca176ab1fcdea" + integrity sha512-xZsFA3uX8MO3yAda03QrG3/Eg1LN3EPfjjf07vke/46HERLZyHrTsQ9E1r1w1W//fWEhtYNndo2hQplN2cVpCQ== + dependencies: + postcss "^7.0.0" + +postcss-selector-matches@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff" + integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww== + dependencies: + balanced-match "^1.0.0" + postcss "^7.0.2" + +postcss-selector-not@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.0.tgz#c68ff7ba96527499e832724a2674d65603b645c0" + integrity sha512-W+bkBZRhqJaYN8XAnbbZPLWMvZD1wKTu0UxtFKdhtGjWYmxhkUneoeOhRJKdAE5V7ZTlnbHfCR+6bNwK9e1dTQ== + dependencies: + balanced-match "^1.0.0" + postcss "^7.0.2" + +postcss-selector-parser@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865" + integrity sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU= + dependencies: + dot-prop "^4.1.1" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^5.0.0, postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" + integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== + dependencies: + cssesc "^2.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" + integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-svgo@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" + integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw== + dependencies: + is-svg "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + svgo "^1.0.0" + +postcss-unique-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" + integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== + dependencies: + alphanum-sort "^1.0.0" + postcss "^7.0.0" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + +postcss-value-parser@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9" + integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ== + +postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" + integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg== + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss@7.0.14: + version "7.0.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5" + integrity sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.17" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f" + integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + +prettier@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" + integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== + +pretty-bytes@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" + integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== + +pretty-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" + integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM= + dependencies: + renderkid "^2.0.1" + utila "~0.4" + +pretty-format@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" + integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== + dependencies: + "@jest/types" "^24.9.0" + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + react-is "^16.8.4" + +pretty-ms@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-5.0.0.tgz#6133a8f55804b208e4728f6aa7bf01085e951e24" + integrity sha512-94VRYjL9k33RzfKiGokPBPpsmloBYSf5Ri+Pq19zlsEcUKFob+admeXr5eFDRuPjFmEOcjJvPGdillYOJyvZ7Q== + dependencies: + parse-ms "^2.1.0" + +private@^0.1.6, private@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +promise@8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.3.tgz#f592e099c6cddc000d538ee7283bb190452b0bf6" + integrity sha512-HeRDUL1RJiLhyA0/grn+PTShlBAcLuh/1BJGtrvjwbvRDCTLLMEz9rOGCV+R3vHY4MixIuoMEd9Yq/XvsTPcjw== + dependencies: + asap "~2.0.6" + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + +prompts@^2.0.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.2.1.tgz#f901dd2a2dfee080359c0e20059b24188d75ad35" + integrity sha512-VObPvJiWPhpZI6C5m60XOzTfnYg/xc/an+r9VYymj9WJW3B/DIH+REzjpAACPf8brwPeP+7vz3bIim3S+AaMjw== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.3" + +prop-types-exact@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869" + integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA== + dependencies: + has "^1.0.3" + object.assign "^4.1.0" + reflect.ownkeys "^0.2.0" + +prop-types@15.x, prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +proxy-addr@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.0" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + +psl@^1.1.24, psl@^1.1.28: + version "1.3.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.3.0.tgz#e1ebf6a3b5564fa8376f3da2275da76d875ca1bd" + integrity sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag== + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s= + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== + +raf@3.4.1, raf@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-apollo@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-3.1.3.tgz#5d8540b401bba36173b63e6c5e75fa561960c63e" + integrity sha512-orCZNoAkgveaK5b75y7fw1MSqSHOU/Wuu9rRFOGmRQBSQVZjvV4DI+hj604rHmuN9+WDABxb5W48wTa0F/xNZQ== + dependencies: + "@apollo/react-common" "^3.1.3" + "@apollo/react-components" "^3.1.3" + "@apollo/react-hoc" "^3.1.3" + "@apollo/react-hooks" "^3.1.3" + "@apollo/react-ssr" "^3.1.3" + +react-app-polyfill@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-1.0.4.tgz#4dd2636846b585c2d842b1e44e1bc29044345874" + integrity sha512-5Vte6ki7jpNsNCUKaboyofAhmURmCn2Y6Hu7ydJ6Iu4dct1CIGoh/1FT7gUZKAbowVX2lxVPlijvp1nKxfAl4w== + dependencies: + core-js "3.2.1" + object-assign "4.1.1" + promise "8.0.3" + raf "3.4.1" + regenerator-runtime "0.13.3" + whatwg-fetch "3.0.0" + +react-color@3.0.0-beta.3: + version "3.0.0-beta.3" + resolved "https://registry.yarnpkg.com/react-color/-/react-color-3.0.0-beta.3.tgz#5b59a1ed09066751409b98206fe265c7ff02d811" + integrity sha1-W1mh7QkGZ1FAm5ggb+Jlx/8C2BE= + dependencies: + lodash "^4.0.1" + material-colors "^1.2.1" + prop-types "^15.5.10" + reactcss "^1.2.3" + tinycolor2 "^1.4.1" + +react-dev-utils@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-9.1.0.tgz#3ad2bb8848a32319d760d0a84c56c14bdaae5e81" + integrity sha512-X2KYF/lIGyGwP/F/oXgGDF24nxDA2KC4b7AFto+eqzc/t838gpSGiaU8trTqHXOohuLxxc5qi1eDzsl9ucPDpg== + dependencies: + "@babel/code-frame" "7.5.5" + address "1.1.2" + browserslist "4.7.0" + chalk "2.4.2" + cross-spawn "6.0.5" + detect-port-alt "1.1.6" + escape-string-regexp "1.0.5" + filesize "3.6.1" + find-up "3.0.0" + fork-ts-checker-webpack-plugin "1.5.0" + global-modules "2.0.0" + globby "8.0.2" + gzip-size "5.1.1" + immer "1.10.0" + inquirer "6.5.0" + is-root "2.1.0" + loader-utils "1.2.3" + open "^6.3.0" + pkg-up "2.0.0" + react-error-overlay "^6.0.3" + recursive-readdir "2.2.2" + shell-quote "1.7.2" + sockjs-client "1.4.0" + strip-ansi "5.2.0" + text-table "0.2.0" + +react-dom@^16.12.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.12.0.tgz#0da4b714b8d13c2038c9396b54a92baea633fe11" + integrity sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.18.0" + +react-draggable@3.x, react-draggable@^3.0.3: + version "3.3.0" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.3.0.tgz#2ed7ea3f92e7d742d747f9e6324860606cd4d997" + integrity sha512-U7/jD0tAW4T0S7DCPK0kkKLyL0z61sC/eqU+NUfDjnq+JtBKaYKDHpsK2wazctiA4alEzCXUnzkREoxppOySVw== + dependencies: + classnames "^2.2.5" + prop-types "^15.6.0" + +react-error-overlay@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.3.tgz#c378c4b0a21e88b2e159a3e62b2f531fd63bf60d" + integrity sha512-bOUvMWFQVk5oz8Ded9Xb7WVdEi3QGLC8tH7HmYP0Fdp4Bn3qw0tRFmr5TW6mvahzvmrK4a6bqWGfCevBflP+Xw== + +react-grid-layout@^0.16.6: + version "0.16.6" + resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.6.tgz#9b2407a2b946c2260ebaf66f13b556e1da4efeb2" + integrity sha512-h2EsYgsqcESLJeevQSJsEKp8hhh+phOlXDJoMhlV2e7T3VWQL+S6iCF3iD/LK19r4oyRyOMDEir0KV+eLXrAyw== + dependencies: + classnames "2.x" + lodash.isequal "^4.0.0" + prop-types "15.x" + react-draggable "3.x" + react-resizable "1.x" + +react-infinite@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/react-infinite/-/react-infinite-0.13.0.tgz#a08f84d800f4a4af5f732724c61200d5142697ea" + integrity sha512-sISd4IYKELmOrvCq9i3FaQo4HR+Bn49ufK0eYAWQAisQ87QWJ5tqiQvEzww+JJZryZVMFvBCuiV7RUn/MfeEww== + dependencies: + enzyme-adapter-react-16 "1.1.1" + lodash.isarray "3.0.4" + lodash.isfinite "3.2.0" + object-assign "4.0.1" + +react-is@^16.5.2, react-is@^16.7.0: + version "16.8.4" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2" + integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA== + +react-is@^16.6.0: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" + integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== + +react-is@^16.8.1, react-is@^16.8.4: + version "16.9.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" + integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== + +react-is@^16.8.6, react-is@^16.9.0: + version "16.10.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.10.2.tgz#984120fd4d16800e9a738208ab1fba422d23b5ab" + integrity sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-reconciler@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.7.0.tgz#9614894103e5f138deeeb5eabaf3ee80eb1d026d" + integrity sha512-50JwZ3yNyMS8fchN+jjWEJOH3Oze7UmhxeoJLn2j6f3NjpfCRbcmih83XTWmzqtar/ivd5f7tvQhvvhism2fgg== + dependencies: + fbjs "^0.8.16" + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.0" + +react-resizable@1.x: + version "1.8.0" + resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.8.0.tgz#a6dd8c90826f1e54244141b770318e0320663a43" + integrity sha512-lfYlCelPyUL8GLYd4SgFtZOrFY8XpxpDwXjgGHLiLM2aMv7mEkBT8Jn5MUV1TCinF5mIro4CMezfnniAZEkGvg== + dependencies: + prop-types "15.x" + react-draggable "^3.0.3" + +react-resize-detector@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-2.3.0.tgz#57bad1ae26a28a62a2ddb678ba6ffdf8fa2b599c" + integrity sha512-oCAddEWWeFWYH5FAcHdBYcZjAw9fMzRUK9sWSx6WvSSOPVRxcHd5zTIGy/mOus+AhN/u6T4TMiWxvq79PywnJQ== + dependencies: + lodash.debounce "^4.0.8" + lodash.throttle "^4.1.1" + prop-types "^15.6.0" + resize-observer-polyfill "^1.5.0" + +react-router-dom@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18" + integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.1.2" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.1.2, react-router@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418" + integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + mini-create-react-context "^0.3.0" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-scripts@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.2.0.tgz#58ccd6b4ffa27f1b4d2986cbdcaa916660e9e33c" + integrity sha512-6LzuKbE2B4eFQG6i1FnTScn9HDcWBfXXnOwW9xKFPJ/E3rK8i1ufbOZ0ocKyRPxJAKdN7iqg3i7lt0+oxkSVOA== + dependencies: + "@babel/core" "7.6.0" + "@svgr/webpack" "4.3.2" + "@typescript-eslint/eslint-plugin" "^2.2.0" + "@typescript-eslint/parser" "^2.2.0" + babel-eslint "10.0.3" + babel-jest "^24.9.0" + babel-loader "8.0.6" + babel-plugin-named-asset-import "^0.3.4" + babel-preset-react-app "^9.0.2" + camelcase "^5.2.0" + case-sensitive-paths-webpack-plugin "2.2.0" + css-loader "2.1.1" + dotenv "6.2.0" + dotenv-expand "5.1.0" + eslint "^6.1.0" + eslint-config-react-app "^5.0.2" + eslint-loader "3.0.2" + eslint-plugin-flowtype "3.13.0" + eslint-plugin-import "2.18.2" + eslint-plugin-jsx-a11y "6.2.3" + eslint-plugin-react "7.14.3" + eslint-plugin-react-hooks "^1.6.1" + file-loader "3.0.1" + fs-extra "7.0.1" + html-webpack-plugin "4.0.0-beta.5" + identity-obj-proxy "3.0.0" + is-wsl "^1.1.0" + jest "24.9.0" + jest-environment-jsdom-fourteen "0.1.0" + jest-resolve "24.9.0" + jest-watch-typeahead "0.4.0" + mini-css-extract-plugin "0.8.0" + optimize-css-assets-webpack-plugin "5.0.3" + pnp-webpack-plugin "1.5.0" + postcss-flexbugs-fixes "4.1.0" + postcss-loader "3.0.0" + postcss-normalize "7.0.1" + postcss-preset-env "6.7.0" + postcss-safe-parser "4.0.1" + react-app-polyfill "^1.0.4" + react-dev-utils "^9.1.0" + resolve "1.12.0" + resolve-url-loader "3.1.0" + sass-loader "7.2.0" + semver "6.3.0" + style-loader "1.0.0" + terser-webpack-plugin "1.4.1" + ts-pnp "1.1.4" + url-loader "2.1.0" + webpack "4.41.0" + webpack-dev-server "3.2.1" + webpack-manifest-plugin "2.1.1" + workbox-webpack-plugin "4.3.1" + optionalDependencies: + fsevents "2.0.7" + +react-smooth@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-1.0.5.tgz#94ae161d7951cdd893ccb7099d031d342cb762ad" + integrity sha512-eW057HT0lFgCKh8ilr0y2JaH2YbNcuEdFpxyg7Gf/qDKk9hqGMyXryZJ8iMGJEuKH0+wxS0ccSsBBB3W8yCn8w== + dependencies: + lodash "~4.17.4" + prop-types "^15.6.0" + raf "^3.4.0" + react-transition-group "^2.5.0" + +react-test-renderer@^16.0.0-0: + version "16.10.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.10.2.tgz#4d8492f8678c9b43b721a7d79ed0840fdae7c518" + integrity sha512-k9Qzyev6cTIcIfrhgrFlYQAFxh5EEDO6ALNqYqmKsWVA7Q/rUMTay5nD3nthi6COmYsd4ghVYyi8U86aoeMqYQ== + dependencies: + object-assign "^4.1.1" + prop-types "^15.6.2" + react-is "^16.8.6" + scheduler "^0.16.2" + +react-transition-group@^2.5.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" + integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" + +react-transition-group@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.1.0.tgz#7b50c0a93a6c127336187252c3c1a70eff3304ce" + integrity sha512-/OITbogb3emGN49WaP7468QGSde7er5w6eIHldIDCSQBq/9QTSCzs8OgpgmOnaUXCXzBUcK1zoZ6DqRlM8CJtA== + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react-transition-group@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683" + integrity sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react@^16.12.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83" + integrity sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + +reactcss@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd" + integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A== + dependencies: + lodash "^4.0.1" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6, readable-stream@^3.1.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" + integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +realpath-native@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" + integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== + dependencies: + util.promisify "^1.0.0" + +recast@^0.18.0: + version "0.18.5" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.18.5.tgz#9d5adbc07983a3c8145f3034812374a493e0fe4d" + integrity sha512-sD1WJrpLQAkXGyQZyGzTM75WJvyAd98II5CHdK3IYbt/cZlU0UzCRVU11nUFNXX9fBVEt4E9ajkMjBlUlG+Oog== + dependencies: + ast-types "0.13.2" + esprima "~4.0.0" + private "^0.1.8" + source-map "~0.6.1" + +recharts-scale@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.2.tgz#b66315d985cd9b80d5f7d977a5aab9a305abc354" + integrity sha512-p/cKt7j17D1CImLgX2f5+6IXLbRHGUQkogIp06VUoci/XkhOQiGSzUrsD1uRmiI7jha4u8XNFOjkHkzzBPivMg== + dependencies: + decimal.js-light "^2.4.1" + +recharts@^1.8.5: + version "1.8.5" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-1.8.5.tgz#ca94a3395550946334a802e35004ceb2583fdb12" + integrity sha512-tM9mprJbXVEBxjM7zHsIy6Cc41oO/pVYqyAsOHLxlJrbNBuLs0PHB3iys2M+RqCF0//k8nJtZF6X6swSkWY3tg== + dependencies: + classnames "^2.2.5" + core-js "^2.6.10" + d3-interpolate "^1.3.0" + d3-scale "^2.1.0" + d3-shape "^1.2.0" + lodash "^4.17.5" + prop-types "^15.6.0" + react-resize-detector "^2.3.0" + react-smooth "^1.0.5" + recharts-scale "^0.4.2" + reduce-css-calc "^1.3.0" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= + dependencies: + resolve "^1.1.6" + +recursive-readdir@2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" + integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== + dependencies: + minimatch "3.0.4" + +redeyed@~2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-2.1.1.tgz#8984b5815d99cb220469c99eeeffe38913e6cc0b" + integrity sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs= + dependencies: + esprima "~4.0.0" + +reduce-css-calc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY= + dependencies: + balanced-match "^0.4.2" + math-expression-evaluator "^1.2.14" + reduce-function-call "^1.0.1" + +reduce-function-call@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" + integrity sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk= + dependencies: + balanced-match "^0.4.2" + +reflect.ownkeys@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" + integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA= + +regenerate-unicode-properties@^8.0.2, regenerate-unicode-properties@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e" + integrity sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA== + dependencies: + regenerate "^1.4.0" + +regenerate@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" + integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== + +regenerator-runtime@0.13.3, regenerator-runtime@^0.13.2: + version "0.13.3" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" + integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== + +regenerator-runtime@^0.10.5: + version "0.10.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" + integrity sha1-M2w+/BIgrc7dosn6tntaeVWjNlg= + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + +regenerator-runtime@^0.12.0: + version "0.12.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" + integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== + +regenerator-transform@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb" + integrity sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ== + dependencies: + private "^0.1.6" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regex-parser@2.2.10: + version "2.2.10" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.10.tgz#9e66a8f73d89a107616e63b39d4deddfee912b37" + integrity sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA== + +regexp-tree@^0.1.6: + version "0.1.11" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.11.tgz#c9c7f00fcf722e0a56c7390983a7a63dd6c272f3" + integrity sha512-7/l/DgapVVDzZobwMCCgMlqiqyLFJ0cduo/j+3BcDJIB+yJdsYCfKuI3l/04NV+H/rfNRdPIDbXNZHM9XvQatg== + +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + +regexpp@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.0.0.tgz#dd63982ee3300e67b41c1956f850aa680d9d330e" + integrity sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g== + +regexpu-core@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.4.tgz#080d9d02289aa87fe1667a4f5136bc98a6aebaae" + integrity sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ== + dependencies: + regenerate "^1.4.0" + regenerate-unicode-properties "^8.0.2" + regjsgen "^0.5.0" + regjsparser "^0.6.0" + unicode-match-property-ecmascript "^1.0.4" + unicode-match-property-value-ecmascript "^1.1.0" + +regexpu-core@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.6.0.tgz#2037c18b327cfce8a6fea2a4ec441f2432afb8b6" + integrity sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg== + dependencies: + regenerate "^1.4.0" + regenerate-unicode-properties "^8.1.0" + regjsgen "^0.5.0" + regjsparser "^0.6.0" + unicode-match-property-ecmascript "^1.0.4" + unicode-match-property-value-ecmascript "^1.1.0" + +regjsgen@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd" + integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA== + +regjsparser@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c" + integrity sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ== + dependencies: + jsesc "~0.5.0" + +relateurl@0.2.x: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +renderkid@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" + integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== + dependencies: + css-select "^1.1.0" + dom-converter "^0.2" + htmlparser2 "^3.3.0" + strip-ansi "^3.0.0" + utila "^0.4.0" + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +request-promise-core@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346" + integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag== + dependencies: + lodash "^4.17.11" + +request-promise-native@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59" + integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w== + dependencies: + request-promise-core "1.1.2" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.87.0, request@^2.88.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-pathname@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" + integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg== + +resolve-url-loader@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-3.1.0.tgz#54d8181d33cd1b66a59544d05cadf8e4aa7d37cc" + integrity sha512-2QcrA+2QgVqsMJ1Hn5NnJXIGCX1clQ1F6QJTqOeiaDw9ACo1G2k+8/shq3mtqne03HOFyskAClqfxKyFBriXZg== + dependencies: + adjust-sourcemap-loader "2.0.0" + camelcase "5.0.0" + compose-function "3.0.3" + convert-source-map "1.6.0" + es6-iterator "2.0.3" + loader-utils "1.2.3" + postcss "7.0.14" + rework "1.0.1" + rework-visit "1.0.0" + source-map "0.6.1" + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= + +resolve@1.12.0, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1: + version "1.12.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" + integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== + dependencies: + path-parse "^1.0.6" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +rework-visit@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rework-visit/-/rework-visit-1.0.0.tgz#9945b2803f219e2f7aca00adb8bc9f640f842c9a" + integrity sha1-mUWygD8hni96ygCtuLyfZA+ELJo= + +rework@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rework/-/rework-1.0.1.tgz#30806a841342b54510aa4110850cd48534144aa7" + integrity sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc= + dependencies: + convert-source-map "^0.3.3" + css "^2.0.0" + +rgb-regex@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" + integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= + +rgba-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" + integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= + +rifm@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be" + integrity sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ== + dependencies: + "@babel/runtime" "^7.3.1" + +rimraf@2.6.3, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= + dependencies: + is-promise "^2.1.0" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= + dependencies: + aproba "^1.1.1" + +rxjs@^6.3.3: + version "6.5.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" + integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== + dependencies: + tslib "^1.9.0" + +rxjs@^6.4.0: + version "6.5.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" + integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== + dependencies: + tslib "^1.9.0" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== + dependencies: + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + +sass-loader@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.2.0.tgz#e34115239309d15b2527cb62b5dfefb62a96ff7f" + integrity sha512-h8yUWaWtsbuIiOCgR9fd9c2lRXZ2uG+h8Dzg/AGNj+Hg/3TO8+BBAW9mEP+mh8ei+qBKqSJ0F1FLlYjNBc61OA== + dependencies: + clone-deep "^4.0.1" + loader-utils "^1.0.1" + neo-async "^2.5.0" + pify "^4.0.1" + semver "^5.5.0" + +sax@^1.2.4, sax@~1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +saxes@^3.1.9: + version "3.1.11" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b" + integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g== + dependencies: + xmlchars "^2.1.1" + +scheduler@^0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.16.2.tgz#f74cd9d33eff6fc554edfb79864868e4819132c1" + integrity sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +scheduler@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4" + integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + +schema-utils@^2.0.0, schema-utils@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.1.0.tgz#940363b6b1ec407800a22951bdcc23363c039393" + integrity sha512-g6SViEZAfGNrToD82ZPUjq52KUPDYc+fN5+g6Euo5mLokl/9Yx14z0Cu4RR1m55HtBXejO0sBt+qw79axN+Fiw== + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + +schema-utils@^2.2.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.5.0.tgz#8f254f618d402cc80257486213c8970edfd7c22f" + integrity sha512-32ISrwW2scPXHUSusP8qMg5dLUawKkyV+/qIEV9JdXKx+rsM6mi8vZY8khg2M69Qom16rtroWXD3Ybtiws38gQ== + dependencies: + ajv "^6.10.2" + ajv-keywords "^3.4.1" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + +selfsigned@^1.9.1: + version "1.10.4" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.4.tgz#cdd7eccfca4ed7635d47a08bf2d5d3074092e2cd" + integrity sha512-9AukTiDmHXGXWtWjembZ5NDmVvP2695EtpgbCsxCa68w3c88B+alqbmZ4O3hZ4VWGXeGWzEVdvqgAJD8DQPCDw== + dependencies: + node-forge "0.7.5" + +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.1: + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + +semver@6.3.0, semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^5.5.0, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +sentence-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-2.1.1.tgz#1f6e2dda39c168bf92d13f86d4a918933f667ed4" + integrity sha1-H24t2jnBaL+S0T+G1KkYkz9mftQ= + dependencies: + no-case "^2.2.0" + upper-case-first "^1.1.2" + +serialize-javascript@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" + integrity sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA== + +serve-index@^1.7.2: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.4, setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-clone@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" + integrity sha1-WQnodLp3EG1zrEFM/sH/yofZcGA= + dependencies: + is-extendable "^0.1.1" + kind-of "^2.0.1" + lazy-cache "^0.2.3" + mixin-object "^2.0.1" + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shell-quote@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" + integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== + +shelljs@0.7.7: + version "0.7.7" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1" + integrity sha1-svXHfvlxSPS09uImguELuoZnz/E= + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb" + integrity sha512-SbEG75TzH8G7eVXFSN5f9EExILKfly7SUvVY5DhhYLvfhKqhDFY0OzevWa/zwak0RLRfWS5AvfMWpd9gJvr5Yg== + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= + +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= + +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +snake-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" + integrity sha1-Qb2xtz8w7GagTU4srRt2OH1NbZ8= + dependencies: + no-case "^2.2.0" + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +sockjs-client@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177" + integrity sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg== + dependencies: + debug "^3.2.5" + eventsource "^1.0.7" + faye-websocket "~0.11.1" + inherits "^2.0.3" + json3 "^3.3.2" + url-parse "^1.4.3" + +sockjs-client@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5" + integrity sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g== + dependencies: + debug "^3.2.5" + eventsource "^1.0.7" + faye-websocket "~0.11.1" + inherits "^2.0.3" + json3 "^3.3.2" + url-parse "^1.4.3" + +sockjs@0.3.19: + version "0.3.19" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" + integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw== + dependencies: + faye-websocket "^0.10.0" + uuid "^3.0.1" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= + dependencies: + is-plain-obj "^1.0.0" + +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.5.6: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@~0.5.12: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.1.tgz#6f12ed1c5db7ea4f24ebb8b89ba58c87c08257f2" + integrity sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +ssri@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" + integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== + dependencies: + figgy-pudding "^3.5.1" + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +stack-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" + integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +stream-browserify@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-each@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" + integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= + +string-length@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" + integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= + dependencies: + astral-regex "^1.0.0" + strip-ansi "^4.0.0" + +string-length@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-3.1.0.tgz#107ef8c23456e187a8abd4a61162ff4ac6e25837" + integrity sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA== + dependencies: + astral-regex "^1.0.0" + strip-ansi "^5.2.0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string.prototype.trimleft@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" + integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" + integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string_decoder@^1.0.0, string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + +strip-ansi@5.2.0, strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-comments@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-1.0.2.tgz#82b9c45e7f05873bee53f37168af930aa368679d" + integrity sha512-kL97alc47hoyIQSV165tTt9rG5dn4w1dNnBhOQ3bOU1Nc1hel09jnXANaHJ7vzHLd4Ju8kseDGzlev96pghLFw== + dependencies: + babel-extract-comments "^1.0.0" + babel-plugin-transform-object-rest-spread "^6.26.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-json-comments@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +style-loader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.0.0.tgz#1d5296f9165e8e2c85d24eee0b7caf9ec8ca1f82" + integrity sha512-B0dOCFwv7/eY31a5PCieNwMgMhVGFe9w+rh7s/Bx8kfFkrth9zfTZquoYvdw8URgiqxObQKcpW51Ugz1HjfdZw== + dependencies: + loader-utils "^1.2.3" + schema-utils "^2.0.1" + +stylehacks@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" + integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^5.0.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +supports-hyperlinks@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7" + integrity sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw== + dependencies: + has-flag "^2.0.0" + supports-color "^5.0.0" + +svg-parser@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.2.tgz#d134cc396fa2681dc64f518330784e98bd801ec8" + integrity sha512-1gtApepKFweigFZj3sGO8KT8LvVZK8io146EzXrpVuWCDAbISz/yMucco3hWTkpZNoPabM+dnMOpy6Swue68Zg== + +svgo@^1.0.0, svgo@^1.2.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.0.tgz#bae51ba95ded9a33a36b7c46ce9c359ae9154313" + integrity sha512-MLfUA6O+qauLDbym+mMZgtXCGRfIxyQoeH6IKVcFslyODEe/ElJNwr0FohQ3xG4C6HK6bk3KYPPXwHVJk3V5NQ== + dependencies: + chalk "^2.4.1" + coa "^2.0.2" + css-select "^2.0.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.33" + csso "^3.5.1" + js-yaml "^3.13.1" + mkdirp "~0.5.1" + object.values "^1.1.0" + sax "~1.2.4" + stable "^0.1.8" + unquote "~1.1.1" + util.promisify "~1.0.0" + +swap-case@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/swap-case/-/swap-case-1.1.2.tgz#c39203a4587385fad3c850a0bd1bcafa081974e3" + integrity sha1-w5IDpFhzhfrTyFCgvRvK+ggZdOM= + dependencies: + lower-case "^1.1.1" + upper-case "^1.1.1" + +symbol-observable@^1.0.2, symbol-observable@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + +symbol-tree@^3.2.2: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +table@5.4.6: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + +table@^5.2.3: + version "5.4.5" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.5.tgz#c8f4ea2d8fee08c0027fac27b0ec0a4fe01dfa42" + integrity sha512-oGa2Hl7CQjfoaogtrOHEJroOcYILTx7BZWLGsJIlzoWmB2zmguhNfPJZsWPKYek/MgCxfco54gEi31d1uN2hFA== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + +tapable@^1.0.0, tapable@^1.1.0, tapable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tar@^4: + version "4.4.10" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1" + integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA== + dependencies: + chownr "^1.1.1" + fs-minipass "^1.2.5" + minipass "^2.3.5" + minizlib "^1.2.1" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.3" + +terser-webpack-plugin@1.4.1, terser-webpack-plugin@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" + integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg== + dependencies: + cacache "^12.0.2" + find-cache-dir "^2.1.0" + is-wsl "^1.1.0" + schema-utils "^1.0.0" + serialize-javascript "^1.7.0" + source-map "^0.6.1" + terser "^4.1.2" + webpack-sources "^1.4.0" + worker-farm "^1.7.0" + +terser@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.1.3.tgz#6074fbcf3517561c3272ea885f422c7a8c32d689" + integrity sha512-on13d+cnpn5bMouZu+J8tPYQecsdRJCJuxFJ+FVoPBoLJgk5bCBkp+Uen2hWyi0KIUm6eDarnlAlH+KgIx/PuQ== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + +test-exclude@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^2.0.0" + +text-table@0.2.0, text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +throat@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" + integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +thunky@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826" + integrity sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow== + +timers-browserify@^2.0.4: + version "2.0.10" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae" + integrity sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg== + dependencies: + setimmediate "^1.0.4" + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= + +tiny-invariant@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463" + integrity sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g== + +tiny-warning@^1.0.0, tiny-warning@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.2.tgz#1dfae771ee1a04396bdfde27a3adcebc6b648b28" + integrity sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q== + +tinycolor2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" + integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g= + +title-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa" + integrity sha1-PhJyFtpY0rxb7PE3q5Ha46fNj6o= + dependencies: + no-case "^2.2.0" + upper-case "^1.0.3" + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + dependencies: + punycode "^2.1.0" + +treeify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" + integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= + +ts-invariant@^0.3.2: + version "0.3.3" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.3.3.tgz#b5742b1885ecf9e29c31a750307480f045ec0b16" + integrity sha512-UReOKsrJFGC9tUblgSRWo+BsVNbEd77Cl6WiV/XpMlkifXwNIJbknViCucHvVZkXSC/mcWeRnIGdY7uprcwvdQ== + dependencies: + tslib "^1.9.3" + +ts-invariant@^0.4.0, ts-invariant@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" + integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA== + dependencies: + tslib "^1.9.3" + +ts-node@^8: + version "8.5.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.5.2.tgz#434f6c893bafe501a30b32ac94ee36809ba2adce" + integrity sha512-W1DK/a6BGoV/D4x/SXXm6TSQx6q3blECUzd5TN+j56YEMX3yPVMpHsICLedUw3DvGF3aTQ8hfdR9AKMaHjIi+A== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "^3.0.0" + +ts-pnp@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.4.tgz#ae27126960ebaefb874c6d7fa4729729ab200d90" + integrity sha512-1J/vefLC+BWSo+qe8OnJQfWTYRS6ingxjwqmHMqaMxXMj7kFtKLgAaYW3JeX3mktjgUL+etlU8/B4VUAUI9QGw== + +ts-pnp@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.2.tgz#be8e4bfce5d00f0f58e0666a82260c34a57af552" + integrity sha512-f5Knjh7XCyRIzoC/z1Su1yLLRrPrFCgtUAh/9fCSP6NKbATwpOL1+idQVXQokK9GRFURn/jYPGPfegIctwunoA== + +tslib@^1, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tslib@^1.8.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== + +tslint-sonarts@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/tslint-sonarts/-/tslint-sonarts-1.9.0.tgz#feb593e92db328c0328b430b838adbe65d504de9" + integrity sha512-CJWt+IiYI8qggb2O/JPkS6CkC5DY1IcqRsm9EHJ+AxoWK70lvtP7jguochyNDMP2vIz/giGdWCfEM39x/I/Vnw== + dependencies: + immutable "^3.8.2" + +tslint@^5.20.1: + version "5.20.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" + integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg== + dependencies: + "@babel/code-frame" "^7.0.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^4.0.1" + glob "^7.1.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + mkdirp "^0.5.1" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.29.0" + +tsutils@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== + dependencies: + tslib "^1.8.1" + +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= + +tty@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tty/-/tty-1.0.1.tgz#e4409ac98b0dd1c50b59ff38e86eac3f0764ee45" + integrity sha1-5ECayYsN0cULWf846G6sPwdk7kU= + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-fest@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" + integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +type@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/type/-/type-1.0.3.tgz#16f5d39f27a2d28d86e48f8981859e9d3296c179" + integrity sha512-51IMtNfVcee8+9GJvj0spSuFcZHe9vSib6Xtgsny1Km9ugyz2mbS08I3rsUIRYgJohFRFU1160sgRodYz378Hg== + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typeface-roboto@^0.0.54: + version "0.0.54" + resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-0.0.54.tgz#8f02c9a18d1cfa7f49381a6ff0d21ff061f38ad2" + integrity sha512-sOFA1FXgP0gOgBYlS6irwq6hHYA370KE3dPlgYEJHL3PJd5X8gQE0RmL79ONif6fL5JZuGDj+rtOrFeOqz5IZQ== + +typescript@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" + integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== + +ua-parser-js@^0.7.18: + version "0.7.19" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b" + integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ== + +uglify-js@3.4.x: + version "3.4.10" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" + integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== + dependencies: + commander "~2.19.0" + source-map "~0.6.1" + +uglify-js@^3.1.4: + version "3.6.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" + integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== + dependencies: + commander "~2.20.0" + source-map "~0.6.1" + +unicode-canonical-property-names-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" + integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== + +unicode-match-property-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" + integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== + dependencies: + unicode-canonical-property-names-ecmascript "^1.0.4" + unicode-property-aliases-ecmascript "^1.0.4" + +unicode-match-property-value-ecmascript@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277" + integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g== + +unicode-property-aliases-ecmascript@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57" + integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" + integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== + +upper-case-first@^1.1.0, upper-case-first@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115" + integrity sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU= + dependencies: + upper-case "^1.1.1" + +upper-case@^1.0.3, upper-case@^1.1.0, upper-case@^1.1.1, upper-case@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-loader@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-2.1.0.tgz#bcc1ecabbd197e913eca23f5e0378e24b4412961" + integrity sha512-kVrp/8VfEm5fUt+fl2E0FQyrpmOYgMEkBsv8+UDP1wFhszECq5JyGF33I7cajlVY90zRZ6MyfgKXngLvHYZX8A== + dependencies: + loader-utils "^1.2.3" + mime "^2.4.4" + schema-utils "^2.0.0" + +url-parse@^1.4.3: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util.promisify@1.0.0, util.promisify@^1.0.0, util.promisify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= + dependencies: + inherits "2.0.1" + +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== + dependencies: + inherits "2.0.3" + +utila@^0.4.0, utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^3.0.1, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +v8-compile-cache@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" + integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +value-equal@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" + integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +vendors@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.3.tgz#a6467781abd366217c050f8202e7e50cc9eef8c0" + integrity sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm-browserify@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" + integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw== + +vscode-jsonrpc@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz#a7bf74ef3254d0a0c272fab15c82128e378b3be9" + integrity sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg== + +vscode-languageserver-protocol@3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.14.1.tgz#b8aab6afae2849c84a8983d39a1cf742417afe2f" + integrity sha512-IL66BLb2g20uIKog5Y2dQ0IiigW0XKrvmWiOvc0yXw80z3tMEzEnHjaGAb3ENuU7MnQqgnYJ1Cl2l9RvNgDi4g== + dependencies: + vscode-jsonrpc "^4.0.0" + vscode-languageserver-types "3.14.0" + +vscode-languageserver-types@3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz#d3b5952246d30e5241592b6dde8280e03942e743" + integrity sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A== + +vscode-languageserver@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-5.2.1.tgz#0d2feddd33f92aadf5da32450df498d52f6f14eb" + integrity sha512-GuayqdKZqAwwaCUjDvMTAVRPJOp/SLON3mJ07eGsx/Iq9HjRymhKWztX41rISqDKhHVVyFM+IywICyZDla6U3A== + dependencies: + vscode-languageserver-protocol "3.14.1" + vscode-uri "^1.0.6" + +vscode-uri@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.6.tgz#6b8f141b0bbc44ad7b07e94f82f168ac7608ad4d" + integrity sha512-sLI2L0uGov3wKVb9EB+vIQBl9tVP90nqRvxSoJ35vI3NjxE8jfsE5DSOhWgSunHSZmKS4OCi2jrtfxK7uyp2ww== + +vscode-uri@^1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.8.tgz#9769aaececae4026fb6e22359cb38946580ded59" + integrity sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ== + +w3c-hr-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" + integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU= + dependencies: + browser-process-hrtime "^0.1.2" + +w3c-xmlserializer@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794" + integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg== + dependencies: + domexception "^1.0.1" + webidl-conversions "^4.0.2" + xml-name-validator "^3.0.0" + +walker@^1.0.7, walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + dependencies: + makeerror "1.0.x" + +watchpack@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" + integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== + dependencies: + chokidar "^2.0.2" + graceful-fs "^4.1.2" + neo-async "^2.5.0" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +webpack-dev-middleware@^3.5.1: + version "3.7.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz#ef751d25f4e9a5c8a35da600c5fda3582b5c6cff" + integrity sha512-qvDesR1QZRIAZHOE3iQ4CXLZZSQ1lAUsSpnQmlB1PBfoN/xdRjmge3Dok0W4IdaVLJOGJy3sGI4sZHwjRU0PCA== + dependencies: + memory-fs "^0.4.1" + mime "^2.4.2" + range-parser "^1.2.1" + webpack-log "^2.0.0" + +webpack-dev-server@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.2.1.tgz#1b45ce3ecfc55b6ebe5e36dab2777c02bc508c4e" + integrity sha512-sjuE4mnmx6JOh9kvSbPYw3u/6uxCLHNWfhWaIPwcXWsvWOPN+nc5baq4i9jui3oOBRXGonK9+OI0jVkaz6/rCw== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.0.0" + compression "^1.5.2" + connect-history-api-fallback "^1.3.0" + debug "^4.1.1" + del "^3.0.0" + express "^4.16.2" + html-entities "^1.2.0" + http-proxy-middleware "^0.19.1" + import-local "^2.0.0" + internal-ip "^4.2.0" + ip "^1.1.5" + killable "^1.0.0" + loglevel "^1.4.1" + opn "^5.1.0" + portfinder "^1.0.9" + schema-utils "^1.0.0" + selfsigned "^1.9.1" + semver "^5.6.0" + serve-index "^1.7.2" + sockjs "0.3.19" + sockjs-client "1.3.0" + spdy "^4.0.0" + strip-ansi "^3.0.0" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.5.1" + webpack-log "^2.0.0" + yargs "12.0.2" + +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" + integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== + dependencies: + ansi-colors "^3.0.0" + uuid "^3.3.2" + +webpack-manifest-plugin@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-2.1.1.tgz#6b3e280327815b83152c79f42d0ca13b665773c4" + integrity sha512-2zqJ6mvc3yoiqfDjghAIpljhLSDh/G7vqGrzYcYqqRCd/ZZZCAuc/YPE5xG0LGpLgDJRhUNV1H+znyyhIxahzA== + dependencies: + fs-extra "^7.0.0" + lodash ">=3.5 <5" + object.entries "^1.1.0" + tapable "^1.0.0" + +webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@4.41.0: + version "4.41.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.0.tgz#db6a254bde671769f7c14e90a1a55e73602fc70b" + integrity sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/wasm-edit" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + acorn "^6.2.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.1" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.1" + watchpack "^1.6.0" + webpack-sources "^1.4.1" + +websocket-driver@>=0.5.1: + version "0.7.3" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" + integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== + dependencies: + http-parser-js ">=0.4.0 <0.4.11" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" + integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + +whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-fetch@3.0.0, whatwg-fetch@>=0.10.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" + integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== + +whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" + integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.9, which@^1.3.0, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +widest-line@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" + integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== + dependencies: + string-width "^2.1.1" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +workbox-background-sync@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-4.3.1.tgz#26821b9bf16e9e37fd1d640289edddc08afd1950" + integrity sha512-1uFkvU8JXi7L7fCHVBEEnc3asPpiAL33kO495UMcD5+arew9IbKW2rV5lpzhoWcm/qhGB89YfO4PmB/0hQwPRg== + dependencies: + workbox-core "^4.3.1" + +workbox-broadcast-update@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-4.3.1.tgz#e2c0280b149e3a504983b757606ad041f332c35b" + integrity sha512-MTSfgzIljpKLTBPROo4IpKjESD86pPFlZwlvVG32Kb70hW+aob4Jxpblud8EhNb1/L5m43DUM4q7C+W6eQMMbA== + dependencies: + workbox-core "^4.3.1" + +workbox-build@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-4.3.1.tgz#414f70fb4d6de47f6538608b80ec52412d233e64" + integrity sha512-UHdwrN3FrDvicM3AqJS/J07X0KXj67R8Cg0waq1MKEOqzo89ap6zh6LmaLnRAjpB+bDIz+7OlPye9iii9KBnxw== + dependencies: + "@babel/runtime" "^7.3.4" + "@hapi/joi" "^15.0.0" + common-tags "^1.8.0" + fs-extra "^4.0.2" + glob "^7.1.3" + lodash.template "^4.4.0" + pretty-bytes "^5.1.0" + stringify-object "^3.3.0" + strip-comments "^1.0.2" + workbox-background-sync "^4.3.1" + workbox-broadcast-update "^4.3.1" + workbox-cacheable-response "^4.3.1" + workbox-core "^4.3.1" + workbox-expiration "^4.3.1" + workbox-google-analytics "^4.3.1" + workbox-navigation-preload "^4.3.1" + workbox-precaching "^4.3.1" + workbox-range-requests "^4.3.1" + workbox-routing "^4.3.1" + workbox-strategies "^4.3.1" + workbox-streams "^4.3.1" + workbox-sw "^4.3.1" + workbox-window "^4.3.1" + +workbox-cacheable-response@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-4.3.1.tgz#f53e079179c095a3f19e5313b284975c91428c91" + integrity sha512-Rp5qlzm6z8IOvnQNkCdO9qrDgDpoPNguovs0H8C+wswLuPgSzSp9p2afb5maUt9R1uTIwOXrVQMmPfPypv+npw== + dependencies: + workbox-core "^4.3.1" + +workbox-core@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-4.3.1.tgz#005d2c6a06a171437afd6ca2904a5727ecd73be6" + integrity sha512-I3C9jlLmMKPxAC1t0ExCq+QoAMd0vAAHULEgRZ7kieCdUd919n53WC0AfvokHNwqRhGn+tIIj7vcb5duCjs2Kg== + +workbox-expiration@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-4.3.1.tgz#d790433562029e56837f341d7f553c4a78ebe921" + integrity sha512-vsJLhgQsQouv9m0rpbXubT5jw0jMQdjpkum0uT+d9tTwhXcEZks7qLfQ9dGSaufTD2eimxbUOJfWLbNQpIDMPw== + dependencies: + workbox-core "^4.3.1" + +workbox-google-analytics@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-4.3.1.tgz#9eda0183b103890b5c256e6f4ea15a1f1548519a" + integrity sha512-xzCjAoKuOb55CBSwQrbyWBKqp35yg1vw9ohIlU2wTy06ZrYfJ8rKochb1MSGlnoBfXGWss3UPzxR5QL5guIFdg== + dependencies: + workbox-background-sync "^4.3.1" + workbox-core "^4.3.1" + workbox-routing "^4.3.1" + workbox-strategies "^4.3.1" + +workbox-navigation-preload@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-4.3.1.tgz#29c8e4db5843803b34cd96dc155f9ebd9afa453d" + integrity sha512-K076n3oFHYp16/C+F8CwrRqD25GitA6Rkd6+qAmLmMv1QHPI2jfDwYqrytOfKfYq42bYtW8Pr21ejZX7GvALOw== + dependencies: + workbox-core "^4.3.1" + +workbox-precaching@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-4.3.1.tgz#9fc45ed122d94bbe1f0ea9584ff5940960771cba" + integrity sha512-piSg/2csPoIi/vPpp48t1q5JLYjMkmg5gsXBQkh/QYapCdVwwmKlU9mHdmy52KsDGIjVaqEUMFvEzn2LRaigqQ== + dependencies: + workbox-core "^4.3.1" + +workbox-range-requests@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-4.3.1.tgz#f8a470188922145cbf0c09a9a2d5e35645244e74" + integrity sha512-S+HhL9+iTFypJZ/yQSl/x2Bf5pWnbXdd3j57xnb0V60FW1LVn9LRZkPtneODklzYuFZv7qK6riZ5BNyc0R0jZA== + dependencies: + workbox-core "^4.3.1" + +workbox-routing@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-4.3.1.tgz#a675841af623e0bb0c67ce4ed8e724ac0bed0cda" + integrity sha512-FkbtrODA4Imsi0p7TW9u9MXuQ5P4pVs1sWHK4dJMMChVROsbEltuE79fBoIk/BCztvOJ7yUpErMKa4z3uQLX+g== + dependencies: + workbox-core "^4.3.1" + +workbox-strategies@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-4.3.1.tgz#d2be03c4ef214c115e1ab29c9c759c9fe3e9e646" + integrity sha512-F/+E57BmVG8dX6dCCopBlkDvvhg/zj6VDs0PigYwSN23L8hseSRwljrceU2WzTvk/+BSYICsWmRq5qHS2UYzhw== + dependencies: + workbox-core "^4.3.1" + +workbox-streams@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-4.3.1.tgz#0b57da70e982572de09c8742dd0cb40a6b7c2cc3" + integrity sha512-4Kisis1f/y0ihf4l3u/+ndMkJkIT4/6UOacU3A4BwZSAC9pQ9vSvJpIi/WFGQRH/uPXvuVjF5c2RfIPQFSS2uA== + dependencies: + workbox-core "^4.3.1" + +workbox-sw@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-4.3.1.tgz#df69e395c479ef4d14499372bcd84c0f5e246164" + integrity sha512-0jXdusCL2uC5gM3yYFT6QMBzKfBr2XTk0g5TPAV4y8IZDyVNDyj1a8uSXy3/XrvkVTmQvLN4O5k3JawGReXr9w== + +workbox-webpack-plugin@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-4.3.1.tgz#47ff5ea1cc074b6c40fb5a86108863a24120d4bd" + integrity sha512-gJ9jd8Mb8wHLbRz9ZvGN57IAmknOipD3W4XNE/Lk/4lqs5Htw4WOQgakQy/o/4CoXQlMCYldaqUg+EJ35l9MEQ== + dependencies: + "@babel/runtime" "^7.0.0" + json-stable-stringify "^1.0.1" + workbox-build "^4.3.1" + +workbox-window@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-4.3.1.tgz#ee6051bf10f06afa5483c9b8dfa0531994ede0f3" + integrity sha512-C5gWKh6I58w3GeSc0wp2Ne+rqVw8qwcmZnQGpjiek8A2wpbxSJb1FdCoQVO+jDJs35bFgo/WETgl1fqgsxN0Hg== + dependencies: + workbox-core "^4.3.1" + +worker-farm@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" + integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== + dependencies: + errno "~0.1.7" + +worker-rpc@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5" + integrity sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg== + dependencies: + microevent.ts "~0.1.1" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrap-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" + integrity sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo= + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + +wrap-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-4.0.0.tgz#b3570d7c70156159a2d42be5cc942e957f7b1131" + integrity sha512-uMTsj9rDb0/7kk1PbcbCcwvHUxp60fGDB/NNXpVa0Q+ic/e7y5+BwTxKfQ33VYgDppSwi/FBzpetYzo8s6tfbg== + dependencies: + ansi-styles "^3.2.0" + string-width "^2.1.1" + strip-ansi "^4.0.0" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529" + integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + +ws@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" + integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== + dependencies: + async-limiter "~1.0.0" + +ws@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" + integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + dependencies: + async-limiter "~1.0.0" + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xmlchars@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.1.1.tgz#ef1a81c05bff629c2280007f12daca21bd6f6c93" + integrity sha512-7hew1RPJ1iIuje/Y01bGD/mXokXxegAgVS+e+E0wSi2ILHQkYAH1+JXARwTjZSM4Z4Z+c73aKspEcqj+zPPL/w== + +xregexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" + integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg== + +xtend@^4.0.0, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^3.0.0, yallist@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" + integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yargs-parser@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + +yargs-parser@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" + integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc" + integrity sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ== + dependencies: + cliui "^4.0.0" + decamelize "^2.0.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^10.1.0" + +yargs@^13.3.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" + integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.1" + +yarn@^1.15.0: + version "1.19.1" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.19.1.tgz#14b92410dd1ba5bab87a12b4a3d807f4569bea97" + integrity sha512-gBnfbL9rYY05Gt0cjJhs/siqQXHYlZalTjK3nXn2QO20xbkIFPob+LlH44ML47GcR4VU9/2dYck1BWFM0Javxw== + +yn@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +zen-observable-ts@^0.8.18, zen-observable-ts@^0.8.20: + version "0.8.20" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz#44091e335d3fcbc97f6497e63e7f57d5b516b163" + integrity sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA== + dependencies: + tslib "^1.9.3" + zen-observable "^0.8.0" + +zen-observable@^0.8.0: + version "0.8.14" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.14.tgz#d33058359d335bc0db1f0af66158b32872af3bf7" + integrity sha512-kQz39uonEjEESwh+qCi83kcC3rZJGh4mrZW7xjkSQYXkq//JZHTtKo+6yuVloTgMtzsIWOJrjIrKvk/dqm0L5g== diff --git a/user/create.go b/user/create.go new file mode 100644 index 0000000..6e6778a --- /dev/null +++ b/user/create.go @@ -0,0 +1,28 @@ +package user + +import ( + "context" + "fmt" + + "github.com/jinzhu/copier" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// CreateUser creates a user. +func (r *ResolverForUser) CreateUser(ctx context.Context, name string, pass string, admin bool) (*gqlmodel.User, error) { + newUser := &model.User{ + Name: name, + Pass: createPassword(pass, r.PassStrength), + Admin: admin, + } + + if !r.DB.Where("name = ?", newUser.Name).Find(&model.User{}).RecordNotFound() { + return nil, fmt.Errorf("user with name '%s' does already exist", newUser.Name) + } + + create := r.DB.Create(&newUser) + gqlUser := &gqlmodel.User{} + copier.Copy(gqlUser, newUser) + return gqlUser, create.Error +} diff --git a/user/create_test.go b/user/create_test.go new file mode 100644 index 0000000..749e3b8 --- /dev/null +++ b/user/create_test.go @@ -0,0 +1,52 @@ +package user + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +func TestGQL_CreateUser_succeeds_addsUser(t *testing.T) { + createPassword = fakePassword + db := test.InMemoryDB(t) + defer db.Close() + + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + user, err := resolver.CreateUser(context.Background(), "jmattheis", "unicorn", true) + + require.Nil(t, err) + expected := &gqlmodel.User{ + Name: "jmattheis", + Admin: true, + ID: 1, + } + require.Equal(t, expected, user) + assertUserExist(t, db, model.User{ + Name: "jmattheis", + Pass: unicornPW, + ID: 1, + Admin: true, + }) + assertUserCount(t, db, 1) +} + +func TestGQL_CreateUser_fails_userAlreadyExists(t *testing.T) { + createPassword = fakePassword + db := test.InMemoryDB(t) + defer db.Close() + db.Create(&model.User{ + Name: "jmattheis", + Pass: unicornPW, + ID: 1, + Admin: true, + }) + + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + _, err := resolver.CreateUser(context.Background(), "jmattheis", "unicorn", true) + require.EqualError(t, err, "user with name 'jmattheis' does already exist") + assertUserCount(t, db, 1) +} diff --git a/user/current.go b/user/current.go new file mode 100644 index 0000000..3c984bb --- /dev/null +++ b/user/current.go @@ -0,0 +1,20 @@ +package user + +import ( + "context" + + "github.com/jinzhu/copier" + "github.com/traggo/server/auth" + "github.com/traggo/server/generated/gqlmodel" +) + +// CurrentUser returns the current user. +func (r *ResolverForUser) CurrentUser(ctx context.Context) (*gqlmodel.User, error) { + user := auth.GetUser(ctx) + if user == nil { + return nil, nil + } + var result gqlmodel.User + copier.Copy(&result, user) + return &result, nil +} diff --git a/user/current_test.go b/user/current_test.go new file mode 100644 index 0000000..30d1db9 --- /dev/null +++ b/user/current_test.go @@ -0,0 +1,39 @@ +package user + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/test" + "github.com/traggo/server/test/fake" +) + +func TestGQL_CurrentUser_withUser(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + resolver := ResolverForUser{DB: db.DB} + result, err := resolver.CurrentUser(fake.UserWithPerm(2, true)) + + require.Nil(t, err) + expected := &gqlmodel.User{ + ID: 2, + Name: "fake", + Admin: true, + } + + require.Equal(t, expected, result) +} + +func TestGQL_CurrentUser_noUser(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + + resolver := ResolverForUser{DB: db.DB} + result, err := resolver.CurrentUser(context.Background()) + + require.Nil(t, err) + require.Nil(t, result) +} diff --git a/user/get.go b/user/get.go new file mode 100644 index 0000000..14e7a1b --- /dev/null +++ b/user/get.go @@ -0,0 +1,18 @@ +package user + +import ( + "context" + + "github.com/jinzhu/copier" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// Users returns all users. +func (r *ResolverForUser) Users(ctx context.Context) ([]*gqlmodel.User, error) { + var users []model.User + find := r.DB.Find(&users) + var result []*gqlmodel.User + copier.Copy(&result, &users) + return result, find.Error +} diff --git a/user/get_test.go b/user/get_test.go new file mode 100644 index 0000000..18144fd --- /dev/null +++ b/user/get_test.go @@ -0,0 +1,35 @@ +package user + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/test" +) + +func TestGQL_Users(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + db.NewUserPass(1, "jmattheis", unicornPW, true) + db.NewUserPass(2, "broderpeters", ponyPW, false) + + users, err := resolver.Users(context.Background()) + + require.Nil(t, err) + expected := []*gqlmodel.User{ + { + Name: "jmattheis", + ID: 1, + Admin: true, + }, + { + Name: "broderpeters", + ID: 2, + Admin: false, + }, + } + require.Equal(t, expected, users) +} diff --git a/user/password/password.go b/user/password/password.go new file mode 100644 index 0000000..9dbaca9 --- /dev/null +++ b/user/password/password.go @@ -0,0 +1,17 @@ +package password + +import "golang.org/x/crypto/bcrypt" + +// CreatePassword returns a hashed version of the given password. +func CreatePassword(pw string, strength int) []byte { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(pw), strength) + if err != nil { + panic(err) + } + return hashedPassword +} + +// ComparePassword compares a hashed password with its possible plaintext equivalent. +func ComparePassword(hashedPassword, password []byte) bool { + return bcrypt.CompareHashAndPassword(hashedPassword, password) == nil +} diff --git a/user/password/password_test.go b/user/password/password_test.go new file mode 100644 index 0000000..85fb0b6 --- /dev/null +++ b/user/password/password_test.go @@ -0,0 +1,21 @@ +package password + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPasswordSuccess(t *testing.T) { + password := CreatePassword("secret", 4) + assert.Equal(t, true, ComparePassword(password, []byte("secret"))) +} + +func TestPasswordFailure(t *testing.T) { + password := CreatePassword("secret", 4) + assert.Equal(t, false, ComparePassword(password, []byte("secretx"))) +} + +func TestBCryptFailure(t *testing.T) { + assert.Panics(t, func() { CreatePassword("secret", 12312) }) +} diff --git a/user/remove.go b/user/remove.go new file mode 100644 index 0000000..946b2e2 --- /dev/null +++ b/user/remove.go @@ -0,0 +1,23 @@ +package user + +import ( + "context" + "fmt" + + "github.com/jinzhu/copier" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// RemoveUser removes a user +func (r *ResolverForUser) RemoveUser(ctx context.Context, id int) (*gqlmodel.User, error) { + user := model.User{ID: id} + if r.DB.Find(&user).RecordNotFound() { + return nil, fmt.Errorf("user with id %d does not exist", user.ID) + } + + remove := r.DB.Delete(&user) + gqlUser := &gqlmodel.User{} + copier.Copy(gqlUser, &user) + return gqlUser, remove.Error +} diff --git a/user/remove_test.go b/user/remove_test.go new file mode 100644 index 0000000..ed6b1f3 --- /dev/null +++ b/user/remove_test.go @@ -0,0 +1,104 @@ +package user + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/test" +) + +func TestGQL_RemoveUser_succeeds_removesUser(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.NewUserPass(1, "jmattheis", unicornPW, true) + + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + _, err := resolver.RemoveUser(context.Background(), 1) + + require.Nil(t, err) + assertUserCount(t, db, 0) +} + +func TestGQL_RemoveUser_succeeds_removesDevices(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(1) + user.NewDevice(2, "abc", "devicename") + other := db.User(2) + other.NewDevice(5, "abcx", "devicename") + + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + _, err := resolver.RemoveUser(context.Background(), 1) + + require.Nil(t, err) + user.AssertHasDevice("devicename", false).AssertExists(false) + other.AssertExists(true).AssertHasDevice("devicename", true) +} + +func TestGQL_RemoveUser_succeeds_removesTags(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(1) + user.NewTagDefinition("oops") + other := db.User(2) + other.NewTagDefinition("oops") + + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + _, err := resolver.RemoveUser(context.Background(), 1) + + require.Nil(t, err) + user.AssertExists(false).AssertHasTagDefinition("oops", false) + other.AssertExists(true).AssertHasTagDefinition("oops", true) +} + +func TestGQL_RemoveUser_succeeds_removesTimeSpans(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(1) + other := db.User(2) + ts := user.TimeSpan("2019-06-11T18:00:00Z", "2019-06-11T18:00:00Z") + ts.Tag("hello", "world") + otherTs := other.TimeSpan("2019-06-11T18:00:00Z", "2019-06-11T18:00:00Z") + otherTs.Tag("hello", "world") + + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + _, err := resolver.RemoveUser(context.Background(), 1) + + require.Nil(t, err) + ts.AssertExists(false).AssertHasTag("hello", "world", false) + otherTs.AssertExists(true).AssertHasTag("hello", "world", true) +} + +func TestGQL_RemoveUser_succeeds_removesDashboard(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + user := db.User(1) + other := db.User(2) + userDB := user.Dashboard("cool") + userDB.Range("cool range") + userDB.Entry("cool entry") + + otherDB := other.Dashboard("cool") + otherDB.Range("cool range") + otherDB.Entry("cool entry") + + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + _, err := resolver.RemoveUser(context.Background(), 1) + + require.Nil(t, err) + userDB.AssertExists(false).AssertHasRange("cool range", false).AssertHasEntry("cool entry", false) + otherDB.AssertExists(true).AssertHasRange("cool range", true).AssertHasEntry("cool entry", true) +} + +func TestGQL_RemoveUser_fails_notExistingUser(t *testing.T) { + db := test.InMemoryDB(t) + defer db.Close() + db.NewUserPass(1, "jmattheis", unicornPW, true) + + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + _, err := resolver.RemoveUser(context.Background(), 3) + + require.EqualError(t, err, "user with id 3 does not exist") + assertUserCount(t, db, 1) +} diff --git a/user/update.go b/user/update.go new file mode 100644 index 0000000..f00d271 --- /dev/null +++ b/user/update.go @@ -0,0 +1,30 @@ +package user + +import ( + "context" + "fmt" + + "github.com/jinzhu/copier" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" +) + +// UpdateUser updates a user. +func (r *ResolverForUser) UpdateUser(ctx context.Context, id int, name string, pass *string, admin bool) (*gqlmodel.User, error) { + user := new(model.User) + if r.DB.Find(user, id).RecordNotFound() { + return nil, fmt.Errorf("user with id %d does not exist", id) + } + + user.Name = name + user.Admin = admin + + if pass != nil { + user.Pass = createPassword(*pass, r.PassStrength) + } + + update := r.DB.Save(user) + gqlUser := &gqlmodel.User{} + copier.Copy(gqlUser, user) + return gqlUser, update.Error +} diff --git a/user/update_test.go b/user/update_test.go new file mode 100644 index 0000000..4cb46f7 --- /dev/null +++ b/user/update_test.go @@ -0,0 +1,77 @@ +package user + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/generated/gqlmodel" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +func TestGQL_UpdateUser_succeeds_updatesUser(t *testing.T) { + createPassword = fakePassword + db := test.InMemoryDB(t) + defer db.Close() + db.NewUserPass(1, "jmattheis", unicornPW, true) + + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + user, err := resolver.UpdateUser(context.Background(), 1, "broder", pointer("pony"), false) + require.Nil(t, err) + + expected := &gqlmodel.User{ + Name: "broder", + ID: 1, + Admin: false, + } + require.Equal(t, expected, user) + assertUserCount(t, db, 1) + assertUserExist(t, db, model.User{ + Name: "broder", + ID: 1, + Admin: false, + Pass: ponyPW, + }) +} + +func TestGQL_UpdateUser_succeeds_preservesPassword(t *testing.T) { + createPassword = fakePassword + db := test.InMemoryDB(t) + defer db.Close() + db.NewUserPass(1, "jmattheis", unicornPW, true) + + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + user, err := resolver.UpdateUser(context.Background(), 1, "broder", nil, false) + require.Nil(t, err) + + expected := &gqlmodel.User{ + Name: "broder", + ID: 1, + Admin: false, + } + require.Equal(t, expected, user) + assertUserCount(t, db, 1) + assertUserExist(t, db, model.User{ + Name: "broder", + ID: 1, + Admin: false, + Pass: unicornPW, + }) +} + +func TestGQL_UpdateUser_fails_notExistingUser(t *testing.T) { + createPassword = fakePassword + db := test.InMemoryDB(t) + defer db.Close() + + resolver := ResolverForUser{DB: db.DB, PassStrength: 4} + _, err := resolver.UpdateUser(context.Background(), 1, "broder", nil, false) + require.EqualError(t, err, "user with id 1 does not exist") + + assertUserCount(t, db, 0) +} + +func pointer(s string) *string { + return &s +} diff --git a/user/userresolver.go b/user/userresolver.go new file mode 100644 index 0000000..dea4e0a --- /dev/null +++ b/user/userresolver.go @@ -0,0 +1,14 @@ +package user + +import ( + "github.com/jinzhu/gorm" + "github.com/traggo/server/user/password" +) + +var createPassword = password.CreatePassword + +// ResolverForUser resolves user specific things. +type ResolverForUser struct { + DB *gorm.DB + PassStrength int +} diff --git a/user/utils_test.go b/user/utils_test.go new file mode 100644 index 0000000..0c84d13 --- /dev/null +++ b/user/utils_test.go @@ -0,0 +1,36 @@ +package user + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/traggo/server/model" + "github.com/traggo/server/test" +) + +var ( + ponyPW = []byte{1} + unicornPW = []byte{2} + fakePassword = func(pw string, strength int) []byte { + if pw == "pony" { + return ponyPW + } else if pw == "unicorn" { + return unicornPW + } + panic("unknown pw") + } +) + +func assertUserExist(t *testing.T, db *test.Database, expected model.User) { + foundUser := new(model.User) + find := db.Find(foundUser, expected.ID) + require.Nil(t, find.Error) + require.NotNil(t, foundUser) + require.Equal(t, expected, *foundUser) +} + +func assertUserCount(t *testing.T, db *test.Database, expected int) { + count := new(int) + db.Model(new(model.User)).Count(count) + require.Equal(t, expected, *count) +}