Showing preview only (246K chars total). Download the full file or copy to clipboard to get everything.
Repository: rgehan/octolenses
Branch: master
Commit: a0727568b17d
Files: 159
Total size: 210.7 KB
Directory structure:
gitextract_cgx_ogxj/
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .nvmrc
├── .parcelrc
├── .prettierrc
├── LICENSE
├── README.md
├── changelog.md
├── cypress/
│ ├── e2e/
│ │ ├── discover.spec.js
│ │ ├── filters.spec.js
│ │ └── settings.spec.js
│ └── support/
│ ├── commands.js
│ └── e2e.js
├── cypress.config.ts
├── jest.config.js
├── manifest.json
├── package.json
├── postcss.config.js
├── scripts/
│ ├── release
│ └── screenshots
├── src/
│ ├── @types/
│ │ ├── contrast/
│ │ │ └── index.d.ts
│ │ └── human-format/
│ │ └── index.d.ts
│ ├── App.tsx
│ ├── components/
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ └── index.ts
│ │ ├── Dropdown/
│ │ │ ├── Dropdown.tsx
│ │ │ └── index.ts
│ │ ├── FilterLink/
│ │ │ ├── FilterLink.tsx
│ │ │ └── index.ts
│ │ ├── FilterPredicate/
│ │ │ ├── FilterPredicate.tsx
│ │ │ ├── OperatorSelector.tsx
│ │ │ ├── ValueSelector.tsx
│ │ │ └── index.ts
│ │ ├── Header/
│ │ │ ├── Header.tsx
│ │ │ ├── TabLink.tsx
│ │ │ └── index.ts
│ │ ├── Loader/
│ │ │ ├── Loader.tsx
│ │ │ └── index.ts
│ │ ├── Modal/
│ │ │ ├── Modal.tsx
│ │ │ └── index.ts
│ │ ├── RadioCard/
│ │ │ ├── RadioCard.tsx
│ │ │ └── index.ts
│ │ ├── ToastManager/
│ │ │ ├── Toast.tsx
│ │ │ ├── ToastManager.tsx
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ └── index.ts
│ ├── constants/
│ │ ├── darkMode.ts
│ │ ├── dates.ts
│ │ └── languages.ts
│ ├── containers/
│ │ ├── FilterEditModal/
│ │ │ ├── FilterEditModal.tsx
│ │ │ ├── PredicatesStep.tsx
│ │ │ ├── ProviderStep.tsx
│ │ │ └── index.ts
│ │ ├── RepoCard/
│ │ │ ├── RepoCard.tsx
│ │ │ └── index.ts
│ │ ├── SettingsModal/
│ │ │ ├── Panel.tsx
│ │ │ ├── SettingsModal.tsx
│ │ │ ├── Sidebar.tsx
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── tabs/
│ │ │ │ ├── Cache.tsx
│ │ │ │ ├── NightMode.tsx
│ │ │ │ └── index.ts
│ │ │ └── types.ts
│ │ └── index.ts
│ ├── errors/
│ │ ├── InvalidCredentials.ts
│ │ ├── NeedTokenError.ts
│ │ ├── RateLimitError.ts
│ │ └── index.ts
│ ├── index.html
│ ├── index.scss
│ ├── index.tsx
│ ├── lib/
│ │ ├── assertUnreachable.ts
│ │ ├── cache.ts
│ │ └── github/
│ │ ├── index.ts
│ │ └── trending/
│ │ └── index.ts
│ ├── migrations/
│ │ ├── index.ts
│ │ ├── mocks/
│ │ │ ├── index.ts
│ │ │ ├── v0.ts
│ │ │ ├── v1-without-token.ts
│ │ │ ├── v1.ts
│ │ │ ├── v2-with-negated-predicates.ts
│ │ │ ├── v2.ts
│ │ │ └── v3-with-defaulted-operators.ts
│ │ ├── testing-utils.ts
│ │ ├── types.ts
│ │ ├── utils.ts
│ │ ├── v0-to-v1.test.ts
│ │ ├── v0-to-v1.ts
│ │ ├── v1-to-v2.test.ts
│ │ ├── v1-to-v2.ts
│ │ ├── v2-to-v3.test.ts
│ │ └── v2-to-v3.ts
│ ├── pages/
│ │ ├── Dashboard/
│ │ │ ├── Dashboard.tsx
│ │ │ ├── FilterLinkContainer.tsx
│ │ │ └── index.ts
│ │ ├── Discover/
│ │ │ ├── Discover.scss
│ │ │ ├── Discover.tsx
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── providers/
│ │ ├── AbstractProvider.ts
│ │ ├── github/
│ │ │ ├── components/
│ │ │ │ ├── IssueCard/
│ │ │ │ │ ├── CheckStatusIndicator.tsx
│ │ │ │ │ ├── ConflictIndicator.tsx
│ │ │ │ │ ├── ContextualDropdown.tsx
│ │ │ │ │ ├── IssueCard.tsx
│ │ │ │ │ ├── IssueStatusIndicator.tsx
│ │ │ │ │ ├── LabelBadge.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── ProfileCard.tsx
│ │ │ │ └── Settings.tsx
│ │ │ ├── fetchers/
│ │ │ │ ├── client.ts
│ │ │ │ ├── graphql/
│ │ │ │ │ ├── query.ts
│ │ │ │ │ ├── search.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── rest/
│ │ │ │ ├── profile.ts
│ │ │ │ └── search.ts
│ │ │ ├── index.tsx
│ │ │ └── predicates/
│ │ │ ├── createdOrUpdatedAt.ts
│ │ │ ├── draft.ts
│ │ │ ├── index.ts
│ │ │ ├── mergeStatus.ts
│ │ │ ├── review.ts
│ │ │ ├── status.ts
│ │ │ └── type.ts
│ │ ├── index.ts
│ │ ├── jira/
│ │ │ ├── components/
│ │ │ │ ├── AvailableResources.tsx
│ │ │ │ ├── IssueCard/
│ │ │ │ │ ├── IssueCard.tsx
│ │ │ │ │ ├── StatusBadge.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── LoginButton.tsx
│ │ │ │ ├── LogoutButton.tsx
│ │ │ │ └── Settings.tsx
│ │ │ ├── fetchers/
│ │ │ │ ├── index.ts
│ │ │ │ ├── refreshToken.ts
│ │ │ │ ├── resources.ts
│ │ │ │ └── swapToken.ts
│ │ │ ├── index.tsx
│ │ │ └── predicates/
│ │ │ └── index.ts
│ │ └── types.ts
│ ├── service_worker/
│ │ ├── cache.js
│ │ └── index.js
│ ├── setupTests.ts
│ └── store/
│ ├── filters.ts
│ ├── index.ts
│ ├── models/
│ │ └── filter.ts
│ ├── navigation.ts
│ ├── settings.ts
│ └── trends.ts
├── tailwind.config.js
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }]
]
}
================================================
FILE: .editorconfig
================================================
root=true
[*]
end_of_line = lf
tab_width = 2
indent_style = space
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
================================================
FILE: .eslintignore
================================================
.cache/
dist/
node_modules/
================================================
FILE: .eslintrc
================================================
{
"extends": [
// Enable eslint recommended, and the few overrides necessary to work with TS
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
// Enable recommended rules specific to TS
"plugin:@typescript-eslint/recommended",
// Enable rules that are types aware
"plugin:@typescript-eslint/recommended-requiring-type-checking",
// React rules
"plugin:react/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"project": "./tsconfig.json"
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
// Enforce `I` prefix for interfaces, as I like it that way
"@typescript-eslint/interface-name-prefix": [
"error",
{
"prefixWithI": "always",
"allowUnderscorePrefix": true
}
],
// Allow using `any`, as it's sometimes easier with external API
"@typescript-eslint/no-explicit-any": "off",
// Do not require return types on all functions, as the inference engine is good enough to figure it out
"@typescript-eslint/explicit-function-return-type": "off",
// It is convenient being able to declare functions after they're used
"@typescript-eslint/no-use-before-define": "off",
// It is erroring when declaring global functions on the window object, which is annoying
"@typescript-eslint/unbound-method": ["off"],
"@typescript-eslint/camelcase": ["error", {
"ignoreDestructuring": true,
"properties": "never"
}]
}
}
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
---
**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.
**Desktop (please complete the following information):**
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
---
**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.
================================================
FILE: .github/workflows/main.yml
================================================
name: Lint & Tests
on: [push]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Restore node_modules folder
uses: actions/cache@v1
with:
path: node_modules
key: ${{ runner.os }}-cache-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-cache-
- name: Install dependencies
run: yarn install
- name: Run ESLint
run: yarn lint
- name: Run tsc
run: tsc --noEmit
- name: Run Jest
run: yarn test
integration-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Run Cypress
uses: cypress-io/github-action@v5
with:
record: true
start: yarn start
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
CYPRESS_GITHUB_TOKEN: ${{ secrets.CYPRESS_GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
node_modules/
.idea/
yarn-error.log
.cache/
.parcel-cache/
dist/
dist.crx
dist.pem
octolenses-*
cypress/videos
================================================
FILE: .nvmrc
================================================
20
================================================
FILE: .parcelrc
================================================
{
"extends": "@parcel/config-default",
"transformers": {
"*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
}
}
================================================
FILE: .prettierrc
================================================
{
"singleQuote": true,
"trailingComma": "es5"
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Renan GEHAN
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[](https://chrome.google.com/webstore/detail/octolenses/ghlblfakaklgkdmfejdlffbmpcaidoci)
[](https://addons.mozilla.org/firefox/addon/github-octolenses/)

# OctoLenses Browser Extension
> Watch your repos and discover awesome things directly from your New Tab page.
As a developer, you shouldn't have to worry about that and instead focus on what
is fundamental: your code
This extension allows you to create very precise filters that will nicely lay
out all the information you need in order to be as productive as possible.



## Installation
[](https://chrome.google.com/webstore/detail/octolenses/ghlblfakaklgkdmfejdlffbmpcaidoci)
[](https://addons.mozilla.org/firefox/addon/github-octolenses/)
Simply download it on your favorite browser's extensions store.
It is available on both [Google Chrome](https://chrome.google.com/webstore/detail/octolenses/ghlblfakaklgkdmfejdlffbmpcaidoci) and [Firefox](https://addons.mozilla.org/firefox/addon/github-octolenses/).
By default, it overrides your browser's default "New Tab" page, but this can be
disabled so that it only opens when you click on the extension's icon.
You can access this setting in the settings modal, which can be opened by
simply clicking on the little cog icon on the top right hand corner.
## Usage example
At my current job, we have quite a lovely system where each Pull Request is
assigned a specific label depending on whether it's a WIP, under review or
if it has been successfully (or not) reviewed.
I built this tool for the very purpose of keeping track of this, but this is not
all it can do. It can do much more, such as:
- Helping you contribute to Open Source by presenting you with issues that are
labelled `Good first issue` or `Help wanted`
- Helping you stay up to date with your favorite framework changes
- Allowing you to discover trendy repositories in your favorite language
(similar to what [GitHunt](https://github.com/kamranahmedse/githunt) does)
## Dark theme
Because being flashed bright lights in the eyes at night is the worst thing
ever, I even included a pretty cool dark mode.




## Extensively configurable
There are a lot of settings you can tweak, to adapt the experience of the
extension to your needs.



## Permissions asked
OctoLenses only asks for the `tabs` permission, as it needs to be able to:
- Detect when a tab is opened so it can eventually override it
- Create a new tab when the extension's icon is clicked.
## Development setup
You need a few tools before being able to build the extension:
- `yarn`, a [JS package manager](https://yarnpkg.com/docs/install) (on Mac: `brew install yarn`)
- `jq`, a [JSON CLI utility](https://stedolan.github.io/jq/) (on Mac: `brew install jq`).
- `sed`, should be available on any Unix system.
- `zip`, should be available on any Unix system.
Then you can follow this process to develop/build the extension:
```sh
# Clone the repository
git clone git@github.com:rgehan/octolenses.git && \
cd octolenses
# Install the dependencies
yarn
# Run the development environment...
yarn start
# ...or build the extension
yarn build
```
The built extension (located in the `dist/` folder), can then be loaded inside
your browser as an _unpacked extension_, provided you're in developer mode.
[https://github.com/rgehan/octolenses](rgehan/octolenses)
## Testing
The extension is covered by unit tests, and integration tests.
Here is how you can run them:
```bash
# Run the unit tests (w/ Jest)
yarn test
# Open the integration tests runner (w/ Cypress)
CYPRESS_GITHUB_TOKEN=<github token> yarn e2e
```
## Releasing
```sh
# Update changelog, increment version number & create release commit and tag
yarn release patch|minor|major
# Build the release .zip archive
yarn build
```
The archive can then be uploaded on the Chrome Store dashboard.
## Contributing
1. Fork it (<https://github.com/rgehan/octolenses/fork>)
2. Create your feature branch (`git checkout -b feature/fooBar`)
3. Commit your changes (`git commit -am 'Add some fooBar'`)
4. Push to the branch (`git push origin feature/fooBar`)
5. Create a new Pull Request
## License
MIT © Renan GEHAN
================================================
FILE: changelog.md
================================================
### v2.3.0
- Allow filtering PRs/issues by relative update/creation date [Renan GEHAN]
### v2.2.2
- Refresh all button, when pressing Meta key [Renan GEHAN]
- Handle draft PRs [Renan GEHAN]
- Fix build script to avoid weird parcel issues [Renan GEHAN]
### v2.2.1
- Add support for draft PRs [Renan GEHAN]
### v2.2.0
- Fix wrap issues with GH label badges [Renan GEHAN]
- Add .idea to .gitignore [Renan GEHAN]
- Update main deps (parcel, tailwind) + node LTS (#219) [GitHub]
### v2.1.2
- Remove unused `storage` permission [Renan GEHAN]
### v2.1.1
- Remove unnecessary `tabs` permission [Renan GEHAN]
### v2.1.0
- use-page-overrides-api (#218) [GitHub]
### v2.0.0
- Fallback to localStorage if chrome.storage not available [Renan GEHAN]
- Upgrade cypress [Renan GEHAN]
- Upgrade cypress github action [Renan GEHAN]
- Fix tsc issues [Renan GEHAN]
- Prettify [Renan GEHAN]
- Fix .gitignore [Renan GEHAN]
- Remove some usage of `localStorage` [Renan GEHAN]
- Update @types/chrome [Renan GEHAN]
- Migrate to manifest v3 [Renan GEHAN]
- Upgrade Cypress [Renan GEHAN]
### v1.2.7
- Upgrade all dependencies [Renan GEHAN]
- Fix predicates deletion [Renan GEHAN]
- [Security] Bump acorn from 5.7.3 to 5.7.4 (#161) [GitHub]
- Bump @babel/core from 7.7.4 to 7.8.7 (#160) [GitHub]
- Bump ts-jest from 24.2.0 to 24.3.0 (#158) [GitHub]
- Bump @babel/polyfill from 7.7.0 to 7.8.3 (#154) [GitHub]
- Bump typescript from 3.7.4 to 3.8.3 (#156) [GitHub]
- Fix lint [Renan Gehan]
### v1.2.6
- Display last activity date in IssueCard [Renan Gehan]
- Bump rimraf from 3.0.0 to 3.0.2 (#150) [GitHub]
- Bump @typescript-eslint/parser from 2.8.0 to 2.19.2 (#152) [GitHub]
### v1.2.5
- Fix Jira resource style in night mode [Renan GEHAN]
- Fix link style [Renan GEHAN]
- Add support project predicate (#151) [GitHub]
- Bump eslint-plugin-react from 7.16.0 to 7.18.3 (#147) [GitHub]
- Bump @babel/plugin-proposal-object-rest-spread from 7.7.4 to 7.8… (#145) [GitHub]
- Bump cypress from 3.6.1 to 3.8.3 (#146) [GitHub]
- Bump mobx from 5.15.0 to 5.15.1 (#137) [Renan GEHAN]
- Bump sass from 1.23.7 to 1.24.2 (#140) [Renan GEHAN]
- Bump typescript from 3.7.2 to 3.7.4 (#139) [Renan GEHAN]
- Bump timeago.js from 3.0.2 to 4.0.1 (#131) [Renan GEHAN]
- Bump object-hash from 2.0.0 to 2.0.1 (#133) [Renan GEHAN]
- [Security] Bump serialize-to-js from 3.0.0 to 3.0.2 (#136) [Renan GEHAN]
- Bump eslint from 6.7.0 to 6.7.2 (#134) [Renan GEHAN]
### v1.2.4
- Fix "Open in New Tab" in recent versions of Chrome [Renan GEHAN]
- add-tsc-check (#130) [GitHub]
- migrate-to-eslint (#129) [GitHub]
- improve-tests (#128) [GitHub]
- Remove forgotten it.only() [Renan GEHAN]
- Throw if the CYPRESS_GITHUB_TOKEN env var isn't defined [Renan GEHAN]
- Cypress tests 2 (#127) [GitHub]
- Bump immer from 1.12.1 to 5.0.0 (#123) [Renan GEHAN]
- Bump @babel/polyfill from 7.6.0 to 7.7.0 (#124) [Renan GEHAN]
- Document cypress tests [Renan GEHAN]
- Bump react from 16.11.0 to 16.12.0 (#125) [Renan GEHAN]
- Cypress tests (#126) [GitHub]
- Bump @babel/core from 7.6.4 to 7.7.2 (#113) [Renan GEHAN]
- Bump @types/jest from 24.0.21 to 24.0.22 (#118) [Renan GEHAN]
- Bump @babel/plugin-proposal-class-properties from 7.5.5 to 7.7.0 (#115) [Renan GEHAN]
- Bump tslint from 5.20.0 to 5.20.1 (#117) [Renan GEHAN]
- Bump rimraf from 2.7.1 to 3.0.0 (#114) [Renan GEHAN]
- Bump mobx from 5.14.2 to 5.15.0 (#116) [Renan GEHAN]
- Bump @types/lodash from 4.14.144 to 4.14.146 (#119) [Renan GEHAN]
- Bump object-hash from 1.3.1 to 2.0.0 (#120) [Renan GEHAN]
- Bump @types/react-sortable-hoc from 0.6.6 to 0.7.1 (#121) [Renan GEHAN]
- Bump prettier from 1.18.2 to 1.19.1 (#122) [Renan GEHAN]
### v1.2.3
- Fix URL of the jira issue cards [Renan GEHAN]
- Upgrade all dependencies [Renan GEHAN]
- Bump tslint-react from 4.0.0 to 4.1.0 (#102) [Renan GEHAN]
- Bump human-format from 0.10.0 to 0.10.1 [Renan GEHAN]
- Bump tslint from 5.19.0 to 5.20.0 [Renan GEHAN]
- Bump @babel/plugin-proposal-class-properties from 7.4.0 to 7.5.5 [Renan GEHAN]
- Bump typescript from 3.6.3 to 3.6.4 [Renan GEHAN]
- Bump prop-types from 15.6.2 to 15.7.2 [Renan GEHAN]
- Bump react-sortable-hoc from 1.5.3 to 1.10.1 [Renan GEHAN]
- Bump puppeteer from 1.18.1 to 2.0.0 [Renan GEHAN]
- Bump tailwindcss from 1.0.4 to 1.1.3 [Renan GEHAN]
- Bump @types/styled-components from 4.1.12 to 4.1.20 [Renan GEHAN]
- [Security] Bump safer-eval from 1.3.3 to 1.3.5 [Renan GEHAN]
- Bump @babel/plugin-proposal-decorators from 7.4.0 to 7.6.0 [Renan GEHAN]
- Bump mobx-react from 6.1.1 to 6.1.4 [Renan GEHAN]
- Bump @types/chrome from 0.0.81 to 0.0.91 [Renan GEHAN]
- Bump sass from 1.17.3 to 1.22.12 [Renan GEHAN]
- Bump ts-jest from 24.0.0 to 24.1.0 [Renan GEHAN]
- Bump @types/classnames from 2.2.7 to 2.2.9 [Renan GEHAN]
- Bump typescript from 3.5.3 to 3.6.3 [Renan GEHAN]
- Bump mixin-deep from 1.3.1 to 1.3.2 [Renan GEHAN]
- Bump lodash from 4.17.13 to 4.17.15 [Renan GEHAN]
- Bump @babel/core from 7.4.0 to 7.6.0 [Renan GEHAN]
- Show priority in Jira issue card [Renan GEHAN]
- Revert "Add renovate.json" [Renan GEHAN]
- Add renovate.json [Renan GEHAN]
- Bump @types/object-hash from 1.2.0 to 1.3.0 [dependabot-preview[bot]]
- Bump @babel/plugin-proposal-object-rest-spread from 7.4.0 to 7.5.5 [dependabot-preview[bot]]
- Bump tslint from 5.14.0 to 5.19.0 [dependabot-preview[bot]]
- Bump @types/lodash from 4.14.123 to 4.14.137 [dependabot-preview[bot]]
- Bump @types/jest from 24.0.11 to 24.0.18 [Renan GEHAN]
- Bump typescript from 3.3.4000 to 3.5.3 [Renan GEHAN]
- Bump react from 16.8.1 to 16.9.0 [dependabot-preview[bot]]
- Bump uuid from 3.3.2 to 3.3.3 [dependabot-preview[bot]]
- Add bottom margin to FilterEditModal to add spacing between CTAs and bottom of screen [Renan GEHAN]
- Add overflow auto to Modal content [Renan GEHAN]
- Bump lodash from 4.17.10 to 4.17.13 [Renan GEHAN]
- Do not show all items as new after editing a filter [Renan GEHAN]
- Move update logic to the filter model [Renan GEHAN]
- Clear filter's notifications when navigating to another filter [Renan GEHAN]
- Generate screenshots with notifications too [Renan GEHAN]
- Automatically select a filter on creation [Renan GEHAN]
- Minor ui fixes on Github profile card [Renan GEHAN]
- Let filters automatically refetch themselves in reaction to their hash changing [Renan GEHAN]
- Move filter fetching logic inside of filter model (better encapsulation) [Renan GEHAN]
- Stop resetting whole filter object when simply editing [Renan GEHAN]
- Fix filter.clone so that it clones all relevant attributes of the model [Renan GEHAN]
### v1.2.2
- Update README with new screenshots [Renan GEHAN]
- Fix text color on dark RepoCard [Renan GEHAN]
- Add a few data-* attributes to make navigation easier with Puppeteer [Renan GEHAN]
- Add a script for automatically taking Chrome Store formatted screenshots [Renan GEHAN]
- Fix editorconfig again [Renan GEHAN]
### v1.2.1
- Fix color on active FilterLink in light mode [Renan GEHAN]
- Remove IsDarkContext, directly import settingsStore instead [Renan GEHAN]
- Stop relying on @inject, directly import stores instead [Renan GEHAN]
- Convert FilterLink to TS [Renan GEHAN]
- Add utils for exporting/import the configuration [Renan GEHAN]
- Remove unused migrations file [Renan GEHAN]
- Split filters store file in two: FilterStore and Filter model [Renan GEHAN]
- Convert Dashboard to Typescript [Renan GEHAN]
- Add small border to indicate which are the new items of a filter [Renan GEHAN]
- In Filter, store all new items IDs, instead of just the count [Renan GEHAN]
- Fix type [Renan GEHAN]
- Fix default filter type predicate [Renan GEHAN]
- Make filter sidebar sticky [Renan GEHAN]
- Also fade out toasts when manually discarded [Renan GEHAN]
- Fix toast z-index so that they are visible above modals [Renan GEHAN]
- Add feedback toast to some actions [Renan GEHAN]
- Add active colors to buttons [Renan GEHAN]
- Minor manual adjustement after Tailwind update [Renan GEHAN]
- Update all css classes to new tailwind format [Renan GEHAN]
- Remove normalize.css [Renan GEHAN]
- Update tailwindcss module to 1.0.4 [Renan GEHAN]
- Fix .editorconfig *facepalm* [Renan GEHAN]
### v1.2.0
- Ensure cache invalidation is persistent across hard refreshes [Renan GEHAN]
- Alter Filter model to compute count of new items since last refresh [Renan GEHAN]
- Add method on provider to resolve UID for filter items [Renan GEHAN]
- Add notification badge to FilterLink [Renan GEHAN]
- Make initialize method on AbstractProvider, abstract [Renan GEHAN]
- Slightly decouple FiltersStore and Filter model [Renan GEHAN]
- Add .editorconfig [Renan GEHAN]
- Allow console.log in tslint.json [Renan GEHAN]
- Persist the selectedFilterId [Renan Gehan]
### v1.1.1
- Fix useNewTabPage setting not being taken into account [Renan Gehan]
### v1.1.0
- Add title to the check status [Renan Gehan]
- Show whether a PR is conflicting with an icon [Renan Gehan]
- Retrieve MergeableStatus from PRs (graphql) [Renan Gehan]
### v1.0.0
- Move IssueStatus enum to its own file [Renan GEHAN]
- Default IssueStatus to Unknown when using Github REST fetcher [Renan Gehan]
- Adapt CheckStatusIndicator to new status format [Renan Gehan]
- Adapt github graphql fetcher to properly format status [Renan Gehan]
- Fetch checkSuites associated to the last commit of a PR [Renan Gehan]
- Add header on github fetcher allowing access to the Previews API [Renan Gehan]
- Use enum for Issue status type [Renan Gehan]
- Use international URL for Firefox [Renan GEHAN]
- Bump handlebars from 4.1.1 to 4.1.2 [Renan GEHAN]
- Bump safer-eval from 1.2.3 to 1.3.3 [Renan GEHAN]
- Create migration for operators [Renan GEHAN]
- Add more jira predicates [Renan GEHAN]
- Allow submitting FilterEditModal by pressing Enter [Renan GEHAN]
- Fix style when in dark mode [Renan GEHAN]
- Adapt filtering UI to the operators [Renan GEHAN]
- Replace `negated` flag on predicates by an `operator` key, allowing richer filters [Renan GEHAN]
- Add testing utils for testing migrations in the browser [Renan GEHAN]
- Add logging in migrations [Renan GEHAN]
- Run migrations on app start [Renan GEHAN]
- Create migration from v1 to v2, with tests [Renan GEHAN]
- Disable ts diagnostics in tests [Renan GEHAN]
- Make ts-jest work with js files too [Renan GEHAN]
- Create migration from v0 to v1, with tests [Renan GEHAN]
- Install jest-localstorage-mock [Renan GEHAN]
- setup jest [Renan GEHAN]
- Run tslint autofix on the whole project [Renan GEHAN]
- Fix tslint setup [Renan GEHAN]
- Upgrade build system dependencies [Renan GEHAN]
- Improve header styling (+ ts rewrite) [Renan GEHAN]
- Add link to the repo [Renan GEHAN]
- Rename octolenses-browser-extension to octolenses (to match new repo url) [Renan GEHAN]
- Cache jira filters too [Renan GEHAN]
- Change jira service url, change jira app client id [Renan GEHAN]
- Cache trending repos response [Renan GEHAN]
- Flush expired cache entries on start [Renan GEHAN]
- Create settings tab for the cache (with a clear button) [Renan GEHAN]
- Cache jira available resources for 5m [Renan GEHAN]
- Create filters in loading state by default [Renan GEHAN]
- Cache results from the GitHub GraphQL endpoint [Renan GEHAN]
- Cache results from the GitHub REST endpoint [Renan GEHAN]
- Add a way to bypass the cache for filter refresh [Renan GEHAN]
- Compute a hash for each filter, taking into account all data that's relevant to fetching [Renan GEHAN]
- Implement a cache class (very similar to laravel Cache facade) [Renan GEHAN]
- Very simple implementation of a caching service worker [Renan GEHAN]
- Very simple implementation of a caching service worker [Renan GEHAN]
- Fetch jira resources on init [Renan GEHAN]
- Properly serialize predicates that can contain whitespaces [Renan GEHAN]
- Use first jira resource by default [Renan GEHAN]
- Only refresh jira token if it's expired, or going to in less than 5m [Renan GEHAN]
- Add key to manifest for a stable dev url [Renan GEHAN]
- Refresh token on init (+ add types) [Renan GEHAN]
- Improve Modal animation [Renan GEHAN]
- Fetch resources on jira connection [Renan GEHAN]
- Extract token swap logic outside of the component [Renan GEHAN]
- Use proper token swap service URL [Renan GEHAN]
- Show jira logout button if the user's connected + it's list of available resources [Renan GEHAN]
- Make github ProfileCard react on token change [Renan GEHAN]
- Display user profile in the Github settings panel if the user's logged in [Renan GEHAN]
- Minor UI fix in settings modal sidebar [Renan GEHAN]
- Fix settings pane that are not from provides [Renan GEHAN]
- On init, fetch Github profile [Renan GEHAN]
- Add a way for providers to run an initialization method on start [Renan GEHAN]
- Pass whole provider to the settings views [Renan GEHAN]
- Create method for fetching github profile [Renan GEHAN]
- Reorganize github fetchers [Renan GEHAN]
- Ensure providers are persisted/hydrated just like regular stores [Renan GEHAN]
- Adapt jira provider to the settings changes [Renan GEHAN]
- Adapt github provider to the settings changes [Renan GEHAN]
- Stop passing settings from the generic settings store to the provider settings views [Renan GEHAN]
- Remove providerSettings from the settings store [Renan GEHAN]
- Add an observable/persistable settings object on each provider, so that they manage their own settings [Renan GEHAN]
- Transform Provider interface to an AbstractProvider abstract class [Renan GEHAN]
- Simplify providers by extracting behavior to other files [Renan GEHAN]
- Temporarily use a working site id [Renan GEHAN]
- Get token at the proper setting key in jira provider [Renan GEHAN]
- Update FilterEditModal to accomodate providers system + new UI [Renan GEHAN]
- Rewrite SettingsModal to dynamically render registered providers settings pane [Renan GEHAN]
- Create Jira provider [Renan GEHAN]
- Move GitHub specific rendering logic to the provider, connect it to the settings so it can fetch with credentials [Renan GEHAN]
- Transform Button component to TS [Renan GEHAN]
- Create generic animated modal to be used for all modals [Renan GEHAN]
- Add additional fonts/sizes to tailwind config [Renan GEHAN]
- Install mobx-react-lite for use with hooks, and a few types [Renan GEHAN]
- Rewrite Root App component in TS and provide isDark context from it [Renan GEHAN]
- Create React context for dark mode [Renan GEHAN]
- Add permission to use 'identity' API [Renan GEHAN]
- Add some missing types [Renan GEHAN]
- Move migrations to a file, make them run earlier. [Renan GEHAN]
- Remove now useless filters lib [Renan GEHAN]
- Fix filter addition/cloning [Renan GEHAN]
- Decouple app from GitHub by introducing a system of providers (+ typescript rewrite) [Renan GEHAN]
- Proper TypeScript/TSlint setup (also install lib types) [Renan GEHAN]
- Fix comments count total computation [Renan GEHAN]
- Show sum of reviews/comments count on the IssueCard [Renan GEHAN]
- Add comments count (and fake reviews count) to the result of the REST query [Renan GEHAN]
- Fetch comments/reviews totalCount in graphql query [Renan GEHAN]
- Fix icon used in IssueCard when using REST endpoint [Renan GEHAN]
- Disable HMR as it makes us hit rate limit really fast in dev [Renan GEHAN]
- Fix issue status icon color when using REST endpoint [Renan Gehan]
- Properly serialize REST filter payload [Renan Gehan]
- Renaming CIStatusIndicator component to CheckStatusIndicator. [Renan GEHAN]
- Adding a new merged status predicate with merged and unmerged values. [Renan GEHAN]
- Renaming STATE_COLORS hash map to ISSUE_STATUS_COLORS. Delete unsued import package. [Renan GEHAN]
- Extraction of issue status indicator logic in a specific IssueStatusIndicator component. [Renan GEHAN]
- Rename StatusIndicator component to CIStatusIndicator. [Renan GEHAN]
- Add merged and unmerged values on status filter. [Renan GEHAN]
- Fix small layout issue [Renan GEHAN]
- Fetch issues using the REST api when no token is set [Renan GEHAN]
- Add initial migration for setting schemaVersion to 1 [Renan GEHAN]
- Add schemaVersion attribute to settings store [Renan GEHAN]
- Fix loader dark color [Renan GEHAN]
- Add cursor:pointer on ContextDropdown trigger [Renan GEHAN]
- 49/use-graphql-api (#57) [GitHub]
### v0.4.3
- Adapt ContextualDropdown to theme [Renan Gehan]
- Use ContextualDropdown in IssueCard [Renan Gehan]
- Implement ContextualDropdown [Renan Gehan]
- Install clipboard dependency [Renan Gehan]
- Upgrade to React 16.8 [Renan Gehan]
### v0.4.2
- Fix FilterLink observer [Renan GEHAN]
- Prevent dragging from too far [Renan GEHAN]
- Fix filter selection [Renan GEHAN]
- Select the moved filter [Renan GEHAN]
- Make filters reorderable [Renan GEHAN]
- Update font-awesome to 5.6 [Renan GEHAN]
- Install react-sortable-hoc [Renan GEHAN]
- Allow removing toast by clicking on it [Renan GEHAN]
- Fix toast colors [Renan GEHAN]
- Show proper error when getting rate-limited [Renan GEHAN]
- To rebase toast [Renan GEHAN]
- Add global ToastManager to the tree [Renan GEHAN]
- Create ToastManager for handling notifications [Renan GEHAN]
- Ignore bundled zips [Renan GEHAN]
### v0.4.1
- Fix FilterLink count badge size [Renan GEHAN]
- Add browser icons in the README.md to guide the user to the link [Renan GEHAN]
- Update screenshots & README.md [Renan GEHAN]
- Change default filter [Renan GEHAN]
### v0.4.0
- Fix image placehold bg color to avoid weird border in dark mode [Renan GEHAN]
- Fix dark mode so that it automatically toggles at night [Renan GEHAN]
- Fix font size [Renan GEHAN]
- Reskin FilterLink & Button components [Renan GEHAN]
- Fix alignment of different pages [Renan GEHAN]
- Darken modal backdrop in dark mode [Renan GEHAN]
- Minor UI tweaks in the Settings modal [Renan GEHAN]
- Adapt FilterPredicate to dark mode [Renan GEHAN]
- Reskin FilterEditModal [Renan GEHAN]
- Reskin settings modal [Renan GEHAN]
- Reskin dropdown [Renan GEHAN]
- Add dark mode support in reskined components [Renan GEHAN]
- Reskin app completely [Renan GEHAN]
- Setup tailwindcss [Renan GEHAN]
- Update compress script to add version to filename [Renan GEHAN]
### v0.3.0
- Showcase dark mode in the README [Renan GEHAN]
- Improve Loader style in dark mode [Renan GEHAN]
- Update yarn.lock with integrity entries [Renan GEHAN]
- Improve layout of the settings modal [Renan GEHAN]
- Add night time detection to toggle dark mode [Renan GEHAN]
- Add a dark theme to all relevant components [Renan GEHAN]
- Add a dark mode option [Renan GEHAN]
### v0.2.3
- Add ellipsis and overflow to repo cards titles (#46) [Renan GEHAN]
- Add margins on Loader & error messages (#43) [GitHub]
- Add refresh filter button (#41) [GitHub]
- Add 'Last 2 Weeks' option to discover date ranges [Renan GEHAN]
- Fix Dashboard container height when there are few cards [Renan GEHAN]
### v0.2.2
- Add icons on the browser_action so it works on FF [Renan GEHAN]
### v0.2.1
- Create util method for checking if url is a new tab url [Renan GEHAN]
- Change new tab url check for firefox (#38) [Renan GEHAN]
### v0.2.0
- Update README to explain why the 'tabs' permission is required [Renan GEHAN]
- Update README to explain how to disable the New Tab override [Renan GEHAN]
- Allow setting the new tab behavior in the SettingsModal [Renan GEHAN]
- Add a browser_action for opening OctoLenses [Renan GEHAN]
- Reorganize background scripts to expose a single entry point [Renan GEHAN]
- Update browser_action title [Renan GEHAN]
- Let the decision of overriding the new tab page to a smart background script [Renan GEHAN]
- Add icons [Renan GEHAN]
- Change github personal access token creation link to prefill needed scope (#34) [Renan GEHAN]
- Add link to the Firefox version [Renan GEHAN]
- Improve README.md build instructions [Renan GEHAN]
### v0.1.5
- Refresh filters/trends when a token is set (fix #31) [Renan GEHAN]
- Fix FilterEditModal title input width [Renan GEHAN]
- Remove unnecessary permissions [Renan GEHAN]
- Fix new filter default id not being applied [Renan GEHAN]
- Ensure a failed filter error is removed once refetched successfully [Renan GEHAN]
- Fix typos in README (#24) [Renan GEHAN]
### v0.1.4
- Add clone filter button (fix #16) [Renan GEHAN]
- Improve filter actions UI in the Dashboard [Renan GEHAN]
- Add default 'is:open' predicate to new filters [Renan GEHAN]
- Ensure there is a filter selected after hydration [Renan GEHAN]
- Use repo id as a key to prevent duplicate key warning when using repo name [Renan GEHAN]
- Remove unused import [Renan GEHAN]
- Fix FilterLink not reacting to loading/error state changes [Renan GEHAN]
- Persist filters definitions, not their data [Renan GEHAN]
- Persist settings and navigation store [Renan GEHAN]
- Install mobx-persist & reorganize store bootstrap logic [Renan GEHAN]
- Replace all rematch models by mobx stores [Renan GEHAN]
- Fix order of babel plugins so that mobx decorators work as expected [Renan GEHAN]
- Replace rematch with mobx [Renan GEHAN]
### v0.1.3
- Update README to reflect changes in release process [Renan GEHAN]
- Create release script (fix #22) [Renan GEHAN]
- Add placeholders to all predicates [Renan GEHAN]
- Increase input predicate min-width to 250px [Renan GEHAN]
- Add all review related filter predicates [Renan GEHAN]
================================================
FILE: cypress/e2e/discover.spec.js
================================================
context('Discover', () => {
beforeEach(() => {
cy.injectGithubToken();
cy.visit(Cypress.env('BASE_URL') + '/');
cy.get('[data-header-link=discover]').click();
});
it('loads a default set of repos', () => {
cy.get('[data-id=loader]');
cy.get('[data-id=repo-card]');
});
it('can change the language', () => {
// Let it load
cy.get('[data-id=loader]');
cy.get('[data-id=repo-card]');
// Change the language to JavaScript
cy.get('[data-id=dropdown-language').select('JavaScript');
// Let it load again, and check it loaded JavaScript repos
cy.get('[data-id=loader]');
cy.get('[data-id=repo-card]').contains('JavaScript');
});
it('can change the period', () => {
// Let it load
cy.get('[data-id=loader]');
cy.get('[data-id=repo-card]');
// Change the language to JavaScript
cy.get('[data-id=dropdown-dateRange').select('Last month');
// Let it load again, and check it loaded JavaScript repos
cy.get('[data-id=loader]');
cy.get('[data-id=repo-card]');
});
it('redirects to the repo on click', () => {
// Let it load, and assert it would have opened in a new tab on click
cy.get('[data-id=loader]');
cy.get('[data-id=repo-card]:first-child [data-id=repo-link]').should(
'have.attr',
'target',
'_blank'
);
});
});
================================================
FILE: cypress/e2e/filters.spec.js
================================================
const DEFAULT_FILTER_NAME = 'OctoLenses Issues';
context('Filters', () => {
beforeEach(() => {
cy.injectGithubToken();
cy.visit(Cypress.env('BASE_URL') + '/');
});
it('sees a default filter', () => {
cy.contains(DEFAULT_FILTER_NAME);
});
it('loads a list of issues from the default filter', () => {
cy.contains('rgehan/octolenses');
});
it('can add a GitHub filter', () => {
createFilter({
name: 'React PRs',
predicates: [
{ name: 'Repository', type: 'repo', value: 'facebook/react' },
{ name: 'Type', type: 'type', value: 'PRs', isDropdown: true },
{ name: 'Status', type: 'status', value: 'Open', isDropdown: true },
],
});
cy.get('[data-id=loader]');
cy.contains('React PRs');
});
it('can delete a filter', () => {
createFilter({
name: 'Laravel stuff',
predicates: [
{ name: 'Repository', type: 'repo', value: 'laravel/framework' },
],
});
cy.contains(DEFAULT_FILTER_NAME).click();
cy.contains('Delete').click();
cy.contains(DEFAULT_FILTER_NAME).should('not.exist');
});
it('can edit a filter', () => {
cy.contains(DEFAULT_FILTER_NAME).click();
cy.contains('Edit').click();
// Rename the filter
cy.get('[data-id=filter-label-input]')
.type('{selectAll}{del}')
.type('Laravel PRs');
// Change the target repository
cy.get(`[data-id=predicate-repo] [data-id=predicate-value-selector]`)
.type('{selectAll}{del}')
.type('laravel/framework');
// Save
cy.contains('Continue').click();
// Check the edited filter is in the sidebar
cy.get('[data-id=filter-links]').contains('Laravel PRs');
// Check we have cards corresponding to the new filter
cy.get('[data-id=filter-results]').contains('laravel/framework');
});
it('can clone a filter', () => {
cy.contains(DEFAULT_FILTER_NAME).click();
cy.contains('Clone').click();
cy.get('[data-id=filter-links]').contains(DEFAULT_FILTER_NAME + ' (Copy)');
});
it('can refresh a filter', () => {
cy.contains(DEFAULT_FILTER_NAME).click();
// Waits for results to be displayed
cy.get('[data-id=filter-results]').contains('rgehan/octolenses');
// Refresh
cy.contains('Refresh').click();
// Wait for a loader to appear
cy.get('[data-id=loader]');
// Check we have results again
cy.get('[data-id=filter-results]').contains('rgehan/octolenses');
});
});
function createFilter({ name, predicates }) {
// Open the filter modal
cy.contains('Add').click();
// Select a GitHub filter
cy.contains('GitHub').click();
cy.contains('Continue').click();
// Rename the filter
cy.get('[data-id=filter-label-input]')
.type('{selectAll}{del}')
.type(name);
predicates.forEach(({ name, type, value, isDropdown = false }) => {
cy.get('[data-id=add-predicate-dropdown]').select(name);
if (isDropdown) {
cy.get(
`[data-id=predicate-${type}] [data-id=predicate-value-selector]`
).select(value);
} else {
cy.get(
`[data-id=predicate-${type}] [data-id=predicate-value-selector]`
).type(value);
}
});
// Save the filter
cy.contains('Continue').click();
}
================================================
FILE: cypress/e2e/settings.spec.js
================================================
context('Discover', () => {
beforeEach(() => {
cy.visit(Cypress.env('BASE_URL') + '/');
cy.get('[data-header-link=settings]').click();
});
it('opens, then closes the settings modal', () => {
cy.contains('Settings');
cy.contains('Close').click();
cy.contains('Settings').should('not.exist');
});
it('changes to night mode', () => {
// Check it's not using dark mode
cy.get('body.dark').should('not.exist');
// Switch to dark mode
cy.contains('Night mode').click();
cy.contains('Always').click();
// Check dark mode is applied
cy.get('body.dark');
});
it('clears the cache', () => {
cy.contains('Cache').click();
cy.contains('Clear cache').click();
cy.contains('Cache was successfully cleared');
});
it('sets the GitHub token', () => {
cy.contains('GitHub').click();
cy.get('input').type('my-token');
cy.contains('Save').click();
cy.contains('Token was saved');
});
});
================================================
FILE: cypress/support/commands.js
================================================
Cypress.Commands.add('injectGithubToken', () => {
cy.window().then(window => {
const token = Cypress.env('GITHUB_TOKEN');
if (!token) {
throw new Error(
'No CYPRESS_GITHUB_TOKEN environement variable was provided!'
);
}
window.localStorage.setItem(
'githubProvider',
JSON.stringify({
settings: {
token,
},
})
);
});
});
================================================
FILE: cypress/support/e2e.js
================================================
import './commands';
================================================
FILE: cypress.config.ts
================================================
import { defineConfig } from 'cypress'
export default defineConfig({
projectId: 'r2fbf6',
watchForFileChanges: false,
env: {
BASE_URL: 'http://localhost:1234',
},
fixturesFolder: false,
e2e: {
setupNodeEvents(on, config) {},
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
},
})
================================================
FILE: jest.config.js
================================================
module.exports = {
preset: 'ts-jest/presets/js-with-ts',
testMatch: ['<rootDir>/src/**/*.test.ts'],
testEnvironment: 'jsdom',
setupFiles: ['./src/setupTests.ts'],
globals: {
'ts-jest': {
diagnostics: false,
},
},
};
================================================
FILE: manifest.json
================================================
{
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq/S9dl/VwNTcp+T5ZLnOH8gOZoMkFTiKNsR6EDv3zUiEImaheCDMzFUhRiyxg1NY55RjB7oMcqFl2RQnMr8YIR4zmCN0VKljgzN4G81mH+tN+lSccKYOPcUed26nzzzSdxryUSg2QfadChtnCfFMaZxkwrQTn4So5pTyr9upPNDvL4BCXG1sxeZHpp5TDIJTNYlKngQzmWGd/yi0QBRwukeobk7nqiXzAoXkG2qHP3A+Y3YrGFsXfB3X1rM/1g8qnUHwYlv/MmLboOmRQdEBsKdstUmmGTQb6FuTAYVkTcaw/wtRHyd+Cdf9jTo2SVdU160zZ3Ngqr7B+cYvEx+htQIDAQAB",
"manifest_version": 3,
"name": "OctoLenses",
"version": "<version>",
"short_name": "OctoLenses Browser Extension",
"description": "Watch your repos and discover awesome things directly from your New Tab page.",
"permissions": ["identity"],
"host_permissions": [
"*://*.github.com/*",
"https://use.fontawesome.com/*",
"https://fonts.googleapis.com/*"
],
"chrome_url_overrides": {
"newtab": "index.html"
},
"background": {
"service_worker": "service_worker/index.js"
},
"action": {
"default_title": "OctoLenses",
"default_icon": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"icons": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}
================================================
FILE: package.json
================================================
{
"name": "github-trending-chrome-extension",
"targets": {
"app": {
"context": "browser",
"distDir": "dist/",
"source": "src/index.html"
},
"service_worker": {
"context": "service-worker",
"distDir": "dist/service_worker/",
"source": "src/service_worker/index.js"
}
},
"browserslist": "> 0.5%, last 2 versions, not dead",
"version": "2.3.0",
"author": "Renan GEHAN <rgehan94@gmail.com>",
"license": "MIT",
"dependencies": {
"@babel/polyfill": "^7.8.3",
"@types/object-hash": "^1.3.0",
"babel-polyfill": "^6.26.0",
"classnames": "^2.2.6",
"clipboard": "^2.0.4",
"contrast": "^1.0.1",
"es6-error": "^4.1.1",
"human-format": "^0.10.1",
"immer": "^5.0.0",
"lodash": "^4.17.15",
"mobx": "^5.15.1",
"mobx-persist": "^0.4.1",
"mobx-react": "^6.1.4",
"moment": "^2.22.2",
"object-hash": "^2.0.1",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.8.0",
"react-sortable-hoc": "^1.10.1",
"recompose": "^0.30.0",
"styled-components": "^4.1.3",
"timeago.js": "^4.0.1",
"uuid": "^3.3.3"
},
"devDependencies": {
"@babel/core": "^7.8.7",
"@babel/plugin-proposal-class-properties": "^7.7.0",
"@babel/plugin-proposal-decorators": "^7.6.0",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@parcel/config-default": "^2.10.0",
"@parcel/transformer-sass": "2.10.0",
"@parcel/transformer-typescript-tsc": "^2.10.0",
"@types/chrome": "^0.0.239",
"@types/classnames": "^2.2.9",
"@types/clipboard": "^2.0.1",
"@types/jest": "^24.0.22",
"@types/lodash": "^4.14.146",
"@types/node": "^20.3.3",
"@types/react-dom": "^16.9.4",
"@types/react-sortable-hoc": "^0.7.1",
"@types/recompose": "^0.30.7",
"@types/styled-components": "^4.1.20",
"@types/uuid": "^3.4.4",
"@typescript-eslint/eslint-plugin": "^2.8.0",
"@typescript-eslint/parser": "^2.19.2",
"autoprefixer": "^10.4.16",
"cypress": "12.16.0",
"eslint": "^6.7.2",
"eslint-plugin-react": "^7.18.3",
"jest": "^24.5.0",
"jest-localstorage-mock": "^2.4.0",
"parcel": "^2.0.0",
"prettier": "^1.19.1",
"process": "^0.11.10",
"puppeteer": "^2.0.0",
"rimraf": "^3.0.2",
"sass": "^1.24.2",
"tailwindcss": "2",
"ts-jest": "^24.3.0",
"typescript": "^3.8.3"
},
"scripts": {
"clean": "rimraf dist/ .parcel-cache",
"start": "parcel serve --no-hmr --no-autoinstall src/index.html",
"copyManifest": "cp manifest.json dist/manifest.json",
"copyIcons": "cp -r icons/ dist/icons/",
"syncManifestVersion": "version=$(jq -r .version package.json) && sed -i '' \"s/<version>/$version/\" dist/manifest.json && echo \"Updated manifest with version: $version\"",
"build:unpacked": "yarn clean && parcel build && yarn copyManifest && yarn copyIcons && yarn syncManifestVersion",
"compress": "cd dist/ && zip -rq ../octolenses-$(jq -r .version ../package.json).zip *",
"build": "yarn build:unpacked && yarn compress",
"release": "./scripts/release",
"screenshots": "./scripts/screenshots",
"test": "jest",
"e2e": "cypress open",
"lint": "eslint --ext ts,tsx src/"
}
}
================================================
FILE: postcss.config.js
================================================
const tailwindcss = require('tailwindcss');
// prettier-ignore
module.exports = {
plugins: [
tailwindcss(),
require('autoprefixer'),
],
};
================================================
FILE: scripts/release
================================================
#!/usr/bin/env node
const path = require('path');
const { execSync } = require('child_process');
const { readFileSync, writeFileSync } = require('fs');
const { includes, isBuffer } = require('lodash');
const semver = require('semver');
const RELEASE_TYPES = ['patch', 'minor', 'major'];
const exec = command => {
const stdout = execSync(command);
return isBuffer(stdout) ? stdout.toString() : stdout;
};
// Ensure the release type is valid
const releaseType = process.argv[2];
if (!includes(RELEASE_TYPES, releaseType)) {
console.log(`Usage: yarn release ${RELEASE_TYPES.join('|')}`);
process.exit(1);
}
// Compute the new version number
const packageJsonPath = path.resolve(__dirname, '../package.json');
const packageJson = require(packageJsonPath);
const currentVersion = packageJson.version;
const newVersion = semver.inc(currentVersion, releaseType);
console.log(`1. Incrementing version from ${currentVersion} to ${newVersion}`);
// Generate and write the changelog
console.log('2. Writing changelog...');
const changelogPath = path.resolve(__dirname, '../changelog.md');
const changelogAdditions = exec(`git log v${currentVersion}..HEAD --pretty="- %s [%cn]"`);
const existingChangelog = readFileSync(changelogPath);
const newChangelog = `### v${newVersion}\n${changelogAdditions}\n${existingChangelog}`;
writeFileSync(changelogPath, newChangelog);
// Update package.json version & create the release commit
console.log('3. Updating package.json version number...');
packageJson.version = newVersion;
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log('4. Creating release commit...')
exec(`git add changelog.md package.json`);
exec(`git commit -m "v${newVersion}"`);
console.log('5. Creating release tag...');
exec(`git tag v${newVersion}`);
console.log('[INFO] To publish the changes, run:');
console.log('$ git push origin HEAD && git push --tags');
================================================
FILE: scripts/screenshots
================================================
#!/usr/bin/env node
const path = require('path');
const Bundler = require('parcel-bundler');
const puppeteer = require('puppeteer');
const { kebabCase } = require('lodash');
(async () => {
const port = await startDevelopmentServer();
const { browser, page } = await setupPuppeteer(port);
await takeScreenshots(page);
console.log('All done.');
browser.close();
process.exit(0);
})();
async function startDevelopmentServer() {
console.log('Starting development server...');
const bundler = new Bundler(path.resolve(__dirname, '../src/index.html'), {
hmr: false,
});
const server = await bundler.serve();
console.log('Started.');
return server.address().port;
}
async function setupPuppeteer(port) {
const browser = await puppeteer.launch({
// headless: false,
// devtools: false,
});
const page = await browser.newPage();
// Setup the viewport
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
await page.goto(`http://localhost:${port}`);
return { browser, page };
}
async function takeScreenshots(page) {
// Move to the app, wait for it to load
const DARK_MODES = ['DISABLED', 'ENABLED'];
const SCENARIOS = [
screenshotDashboard,
screenshotDiscover,
screenshotFilterEditModal,
screenshotFilterAddModal,
screenshotSettings,
];
for (darkMode of DARK_MODES) {
console.log(`[darkMode: ${darkMode}]`);
await page.evaluate(darkMode => {
window.stores.settingsStore.darkMode = darkMode;
}, darkMode);
const screenshotFolder = path.resolve(
__dirname,
'../.github/screenshots/',
darkMode === 'ENABLED' ? 'dark' : 'light'
);
for (scenario of SCENARIOS) {
await scenario(page, screenshotFolder);
}
}
}
async function screenshotDashboard(page, screenshotFolder) {
console.log('Capturing screenshot of the Dashboard page...');
await page.waitForFunction(() => !document.querySelector('[data-id=loader]'));
await page.waitFor(300);
await page.evaluate(() => {
const filter = window.stores.filtersStore.data[0];
const identifiers = filter.data.slice(0, 2).map(filter => filter.number);
filter.newItemsIdentifiers = identifiers;
});
await page.screenshot({
path: path.resolve(screenshotFolder, './dashboard.png'),
});
}
async function screenshotDiscover(page, screenshotFolder) {
console.log('Capturing screenshot of the Discover page...');
await page.click('[data-header-link=discover]');
await page.waitForFunction(() => !document.querySelector('[data-id=loader]'));
await page.screenshot({
path: path.resolve(screenshotFolder, './discover.png'),
});
await page.click('[data-header-link=dashboard]');
}
async function screenshotFilterEditModal(page, screenshotFolder) {
console.log('Capturing screenshot of the filter edition modal...');
await page.click('.fa-edit');
await page.waitFor(500);
await page.screenshot({
path: path.resolve(screenshotFolder, './filter-edit.png'),
});
await page.keyboard.press('Escape');
}
async function screenshotFilterAddModal(page, screenshotFolder) {
console.log('Capturing screenshot of the filter addition modal...');
await page.click('.fa-plus-square');
await page.waitFor(500);
await page.screenshot({
path: path.resolve(screenshotFolder, './filter-add.png'),
});
await page.keyboard.press('Escape');
}
async function screenshotSettings(page, screenshotFolder) {
console.log('Capturing screenshot of the night mode settings modal...');
// Open the settings modal
await page.click('.fa-cog');
await page.waitFor(300);
// Screenshot all tabs
const SETTING_TABS = ['Night mode', 'Cache', 'GitHub', 'Jira'];
for (tab of SETTING_TABS) {
await page.click(`[data-setting-tab="${tab}"]`);
await page.screenshot({
path: path.resolve(screenshotFolder, `./settings-${kebabCase(tab)}.png`),
});
}
await page.keyboard.press('Escape');
}
================================================
FILE: src/@types/contrast/index.d.ts
================================================
declare module 'contrast';
================================================
FILE: src/@types/human-format/index.d.ts
================================================
declare module 'human-format';
================================================
FILE: src/App.tsx
================================================
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import { Header, ToastManager } from './components';
import { Dashboard, Discover } from './pages';
import { NavigationStore } from './store/navigation';
const PAGES = {
discover: Discover,
dashboard: Dashboard,
};
type PageName = keyof typeof PAGES;
interface IInnerProps {
navigationStore: NavigationStore;
}
export const App = compose<IInnerProps, {}>(
inject('navigationStore'),
observer
)(({ navigationStore }) => {
const Page = PAGES[navigationStore.page as PageName];
return (
<div className="App">
<Header />
<Page />
<ToastManager />
</div>
);
});
================================================
FILE: src/components/Button/Button.tsx
================================================
import cx from 'classnames';
import React, { ReactNode } from 'react';
export enum ButtonType {
PRIMARY = 'primary',
DEFAULT = 'default',
}
const TYPE_TO_CLASSES: Record<ButtonType, string> = {
primary: 'bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white',
default: 'bg-gray-200 hover:bg-gray-400 active:bg-gray-500 text-black',
};
interface IProps {
type?: ButtonType;
onClick: () => void;
children: ReactNode;
className?: string;
}
export const Button = ({
type = ButtonType.DEFAULT,
onClick,
children,
className = '',
}: IProps) => (
<button
className={cx(
'min-w-24 px-3 py-2 rounded cursor-pointer',
'outline-none hover:outline-none focus:outline-none active:outline-none',
TYPE_TO_CLASSES[type],
className
)}
onClick={onClick}
>
{children}
</button>
);
================================================
FILE: src/components/Button/index.ts
================================================
export { Button, ButtonType } from './Button';
================================================
FILE: src/components/Dropdown/Dropdown.tsx
================================================
import cx from 'classnames';
import { inject, observer } from 'mobx-react';
import React, { ChangeEvent } from 'react';
import { compose } from 'recompose';
import { SettingsStore } from '../../store/settings';
interface IProps {
value: string;
name: string;
items: IOption[];
onChange: (option: IOption) => void;
className?: string;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
interface IOption {
value: string;
name: string;
}
export const Dropdown = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ name, items, value, className, settingsStore, onChange }) => {
function handleChange(event: ChangeEvent<HTMLSelectElement>) {
onChange({ name, value: event.target.value });
}
return (
<div
className={cx(
'w-48 flex relative rounded shadow-lg',
settingsStore.isDark ? 'bg-gray-800 text-white' : 'bg-white',
className
)}
>
<select
onChange={handleChange}
value={value}
className={cx(
'flex-1 border-none bg-transparent cursor-pointer outline-none appearance-none py-2 px-3',
settingsStore.isDark && 'text-white'
)}
data-id={`dropdown-${name}`}
>
{items.map(item => (
<option key={item.value} value={item.value}>
{item.name}
</option>
))}
</select>
<i className="fa fa-caret-down absolute right-0 mt-2 mr-3" />
</div>
);
});
================================================
FILE: src/components/Dropdown/index.ts
================================================
export { Dropdown } from './Dropdown';
================================================
FILE: src/components/FilterLink/FilterLink.tsx
================================================
import cx from 'classnames';
import { size } from 'lodash';
import { inject, observer } from 'mobx-react';
import React from 'react';
import {
SortableElement,
SortableHandle,
SortableElementProps,
} from 'react-sortable-hoc';
import { compose } from 'recompose';
import { Filter } from '../../store/models/filter';
import { SettingsStore } from '../../store/settings';
import { Loader } from '../Loader';
const DragHandle = SortableHandle(
observer(({ isDark }: { isDark: boolean }) => (
<i
className={cx(
'fas fa-grip-horizontal text-sm mr-1',
isDark ? 'text-gray-700' : 'text-gray-500'
)}
/>
))
);
interface IProps extends SortableElementProps {
filter: Filter;
isSelected: boolean;
onClick: () => void;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const FilterLink = compose<IInnerProps, IProps>(
SortableElement,
inject('settingsStore'),
observer
)(({ filter, isSelected, onClick, settingsStore }) => {
const { loading, error } = filter;
const activeColor = settingsStore.isDark ? 'text-gray-500' : 'text-gray-900';
return (
<div
key={filter.id}
className={cx(
'flex items-center text-right rtl pr-4 mb-3 cursor-pointer whitespace-nowrap select-none',
`hover:${activeColor}`,
isSelected ? activeColor : 'text-gray-600'
)}
onClick={onClick}
>
<span
className={cx(
'rounded-full flex-shrink-0 flex items-center justify-center text-xs h-4 w-8 ml-2 relative',
settingsStore.isDark ? 'bg-gray-800' : 'bg-gray-400'
)}
>
{loading && <Loader size={13} strokeWidth={12} />}
{!loading && error && <i className="fa fa-times" />}
{!loading && !error && size(filter.data)}
{filter.newItemsCount > 0 && !filter.loading && (
<div className="absolute right-0 top-0 w-4 h-4 -mr-2 -mt-2 bg-red-600 text-white rounded-full flex items-center justify-center">
{filter.newItemsCount <= 99 ? (
<span className="text-2xs">{filter.newItemsCount}</span>
) : (
<span className="text-base">•</span>
)}
</div>
)}
</span>
<bdi>{filter.label}</bdi>
<DragHandle isDark={settingsStore.isDark} />
</div>
);
});
================================================
FILE: src/components/FilterLink/index.ts
================================================
export { FilterLink } from './FilterLink';
================================================
FILE: src/components/FilterPredicate/FilterPredicate.tsx
================================================
import cx from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import styled from 'styled-components';
import { AbstractProvider } from '../../providers';
import { SettingsStore } from '../../store/settings';
import { OperatorSelector } from './OperatorSelector';
import { ValueSelector } from './ValueSelector';
const Wrapper = styled.div`
.action-icon {
display: none;
}
:hover {
.action-icon {
display: initial;
}
}
`;
interface IProps {
type: string;
operator: string;
value: string;
provider: AbstractProvider;
onChange: (payload: object) => void;
onDelete: () => void;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const FilterPredicate = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ type, operator, value, provider, onDelete, onChange, settingsStore }) => {
const predicate = provider.findPredicate(type);
const handleChange = (key: string) => (newValue: string) =>
onChange({
type,
operator,
value,
[key]: newValue,
});
return (
<Wrapper className="flex relative mb-3" data-id={`predicate-${type}`}>
<div
className={cx(
'flex-1 flex items-stretch rounded overflow-hidden text-lg',
settingsStore.isDark ? 'bg-gray-800' : 'bg-gray-200'
)}
>
<div
className={cx(
'text-white py-2 px-3',
settingsStore.isDark ? 'bg-gray-700' : 'bg-gray-600'
)}
>
<span>{predicate.label}</span>
<OperatorSelector
predicate={predicate}
value={operator}
onChange={handleChange('operator')}
/>
</div>
<div className="flex-1 flex">
<ValueSelector
predicate={predicate}
value={value}
onChange={handleChange('value')}
/>
</div>
</div>
<div
className="absolute inset-y-0 flex items-center px-4"
style={{ left: '100%' }}
>
<i
className={cx(
'action-icon far fa-trash-alt cursor-pointer text-gray-600',
settingsStore.isDark ? 'hover:text-gray-500' : 'hover:text-gray-800'
)}
onClick={onDelete}
/>
</div>
</Wrapper>
);
});
================================================
FILE: src/components/FilterPredicate/OperatorSelector.tsx
================================================
import React from 'react';
import { Predicate } from '../../providers';
interface IProps {
predicate: Predicate;
value: string;
onChange: (value: string) => void;
}
export const OperatorSelector = ({ predicate, value, onChange }: IProps) => {
if (predicate.operators.length === 0) {
return null;
}
return (
<select
className="ml-2 outline-none text-black"
value={value}
onChange={event => onChange(event.target.value)}
data-id="operator-dropdown"
>
{predicate.operators.map(item => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
);
};
================================================
FILE: src/components/FilterPredicate/ValueSelector.tsx
================================================
import cx from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import { Predicate, PredicateType } from '../../providers';
import { SettingsStore } from '../../store/settings';
interface IProps {
predicate: Predicate;
value: string;
onChange: (value: string) => void;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const ValueSelector = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ predicate, value, onChange, settingsStore }) => {
const baseStyle = cx(
'h-full flex-1 bg-transparent outline-none',
settingsStore.isDark ? 'text-white' : 'text-gray-800'
);
if (predicate.type === PredicateType.TEXT) {
return (
<input
type="text"
value={value}
onChange={event => onChange(event.target.value)}
placeholder={predicate.placeholder}
className={cx(baseStyle, 'pl-3')}
data-id="predicate-value-selector"
/>
);
}
if (predicate.type === PredicateType.DROPDOWN) {
return (
<select
value={value}
onChange={event => onChange(event.target.value)}
className={cx(baseStyle, 'ml-2 mr-3')}
data-id="predicate-value-selector"
>
<option key="__default" value="">
Choose...
</option>
{predicate.choices.map(choice => (
<option key={choice.value} value={choice.value}>
{choice.label}
</option>
))}
</select>
);
}
return null;
});
================================================
FILE: src/components/FilterPredicate/index.ts
================================================
export { FilterPredicate } from './FilterPredicate';
================================================
FILE: src/components/Header/Header.tsx
================================================
import cx from 'classnames';
import { capitalize } from 'lodash';
import { inject, observer } from 'mobx-react';
import React, { useState } from 'react';
import { compose } from 'recompose';
import { SettingsModal } from '../../containers';
import { NavigationStore } from '../../store/navigation';
import { SettingsStore } from '../../store/settings';
import { TabLink } from './TabLink';
interface IInnerProps {
settingsStore: SettingsStore;
navigationStore: NavigationStore;
}
export const Header = compose<IInnerProps, {}>(
inject('settingsStore', 'navigationStore'),
observer
)(({ settingsStore, navigationStore }) => {
const [modalOpen, setModalOpen] = useState(false);
function renderLink(name: string) {
const { page, navigateTo } = navigationStore;
return (
<TabLink
onClick={() => navigateTo(name)}
active={page === name}
name={name}
>
{capitalize(name)}
</TabLink>
);
}
return (
<React.Fragment>
<div className="h-24 flex flex-col Header">
<div className="flex items-center">
<a
href="https://github.com/rgehan/octolenses"
className={cx(
'font-roboto text-4xl font-bold mt-4',
settingsStore.isDark ? 'text-white' : 'text-gray-900'
)}
>
<i className="fab fa-github mr-3" />
<span>OctoLenses</span>
</a>
<div className="tabs flex-1 flex justify-end">
{renderLink('dashboard')}
{renderLink('discover')}
<TabLink onClick={() => setModalOpen(true)} name="settings">
<i className="fa fa-cog" />
</TabLink>
</div>
</div>
</div>
{modalOpen && <SettingsModal onClose={() => setModalOpen(false)} />}
</React.Fragment>
);
});
================================================
FILE: src/components/Header/TabLink.tsx
================================================
import cx from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import { SettingsStore } from '../../store/settings';
const COLORS = {
dark: {
active: 'text-white',
inactive: 'text-gray-400 hover:text-white',
},
light: {
active: 'text-gray-800',
inactive: 'text-gray-600 hover:text-gray-800',
},
};
interface IProps {
onClick: () => void;
name: string;
active?: boolean;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const TabLink = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ children, name, onClick, active = false, settingsStore }) => (
<a
className={cx(
'font-roboto ml-4 py-2 cursor-pointer',
COLORS[settingsStore.isDark ? 'dark' : 'light'][
active ? 'active' : 'inactive'
]
)}
onClick={onClick}
data-header-link={name}
>
{children}
</a>
));
================================================
FILE: src/components/Header/index.ts
================================================
export { Header } from './Header';
================================================
FILE: src/components/Loader/Loader.tsx
================================================
import cx from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import { SettingsStore } from '../../store/settings';
interface IProps {
size?: number;
strokeWidth?: number;
className?: string;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const Loader = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ size = 50, strokeWidth = 10, className, settingsStore }) => (
<div
data-id="loader"
className={cx('flex items-center justify-center h-full w-full', className)}
>
<svg
width={`${size}px`}
height={`${size}px`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
className="lds-rolling"
style={{ background: 'none' }}
>
<circle
cx="50"
cy="50"
fill="none"
stroke={settingsStore.isDark ? '#606f7b' : '#678bc2'}
strokeWidth={strokeWidth}
r="35"
strokeDasharray="164.93361431346415 56.97787143782138"
transform="rotate(41.8255 50 50)"
>
<animateTransform
attributeName="transform"
type="rotate"
calcMode="linear"
values="0 50 50;360 50 50"
keyTimes="0;1"
dur="1s"
begin="0s"
repeatCount="indefinite"
/>
</circle>
</svg>
</div>
));
================================================
FILE: src/components/Loader/index.ts
================================================
export { Loader } from './Loader';
================================================
FILE: src/components/Modal/Modal.tsx
================================================
import cx from 'classnames';
import { inject, observer } from 'mobx-react';
import React, { ReactNode, useEffect } from 'react';
import { compose } from 'recompose';
import styled, { keyframes } from 'styled-components';
import { SettingsStore } from '../../store/settings';
const fadeIn = keyframes`
from { opacity: 0; }
to { opacity: 1; }
`;
const Backdrop = styled.div`
animation: ${fadeIn} 0.25s ease;
`;
const slideBottom = keyframes`
from { transform: translateY(100%); }
to { transform: translateY(0); }
`;
const Wrapper = styled.div`
animation: ${slideBottom} 0.25s ease;
`;
interface IProps {
children: ReactNode;
onClose: () => void;
className?: string;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const Modal = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ children, onClose, settingsStore }) => {
// Close the modal on ESC
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
onClose();
}
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
return (
<Backdrop
className={cx(
'fixed z-50 inset-0 font-roboto text-lg',
settingsStore.isDark ? 'bg-gray-900' : 'bg-white'
)}
>
<div
onClick={onClose}
className={cx(
'flex items-center absolute top-0 right-0 mt-4 mr-4 cursor-pointer py-1 px-2 rounded-full',
settingsStore.isDark
? 'text-gray-500 hover:bg-gray-800'
: 'text-gray-700 hover:bg-gray-200'
)}
>
<span className="mr-2">Close</span>
<i className="fa fa-times" />
</div>
<Wrapper className="h-full overflow-auto">{children}</Wrapper>
</Backdrop>
);
});
================================================
FILE: src/components/Modal/index.ts
================================================
export { Modal } from './Modal';
================================================
FILE: src/components/RadioCard/RadioCard.tsx
================================================
import cx from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import { SettingsStore } from '../../store/settings';
interface IProps {
title: string;
text: string;
selected: boolean;
icon?: string;
dark?: boolean;
onClick: () => void;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
const COLORS = {
dark: {
active: 'bg-gray-800 border-blue-500',
inactive: 'bg-gray-800 border-gray-800',
},
light: {
active: 'border-blue-500 bg-blue-100 text-blue-800',
inactive: 'border-gray bg-gray-100 text-gray-800',
},
};
export const RadioCard = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ title, text, selected, icon, onClick, settingsStore }) => (
<div
onClick={onClick}
className={cx(
'h-28 flex border px-3 py-2 rounded cursor-pointer select-none mb-3',
COLORS[settingsStore.isDark ? 'dark' : 'light'][
selected ? 'active' : 'inactive'
]
)}
>
<div className="pr-2 pt-px">
<input type="radio" checked={selected} />
</div>
<div className="leading-normal flex-1">
<div className="font-medium">{title}</div>
<div className="mt-1">{text}</div>
</div>
{icon && (
<div className="w-20 flex items-center justify-center ml-2">
<i
className={cx(
'text-5xl',
selected ? 'text-blue-500' : 'text-gray-400',
icon
)}
/>
</div>
)}
</div>
));
================================================
FILE: src/components/RadioCard/index.ts
================================================
export { RadioCard } from './RadioCard';
================================================
FILE: src/components/ToastManager/Toast.tsx
================================================
import cx from 'classnames';
import React from 'react';
import styled from 'styled-components';
import { INotification } from './types';
const TYPES_TO_ICON = {
info: 'fa-info-circle',
error: 'fa-exclamation-circle',
};
const TYPES_TO_THEME = {
info: 'bg-blue-500 text-white',
error: 'bg-red-400 text-white',
};
const TOAST_DURATION = 3000;
const TOAST_FADE_DURATION = 200;
export const ToastTypes = Object.keys(TYPES_TO_ICON);
const Wrapper = styled.div`
transition: opacity 0.2s;
`;
interface IProps extends INotification {
onRemove: (id: string) => void;
}
export class Toast extends React.Component<IProps> {
public state = {
visible: true,
};
public componentDidMount() {
setTimeout(this.discardToast, TOAST_DURATION);
}
public discardToast = () => {
this.setState({ visible: false });
setTimeout(this.removeToast, TOAST_FADE_DURATION);
};
public removeToast = () => {
const { id, onRemove } = this.props;
onRemove(id);
};
public render() {
const { message, type } = this.props;
const { visible } = this.state;
return (
<Wrapper
onClick={this.discardToast}
className={cx(
'bg-gray-800 px-4 py-3 rounded shadow-md mt-3 select-none pointer-events-auto cursor-pointer',
TYPES_TO_THEME[type],
!visible && 'opacity-0'
)}
>
<i className={cx('fas mr-2', TYPES_TO_ICON[type])} />
<span>{message}</span>
</Wrapper>
);
}
}
================================================
FILE: src/components/ToastManager/ToastManager.tsx
================================================
import { reject } from 'lodash';
import React from 'react';
import { Toast } from './Toast';
import { INotification, NotificationType } from './types';
// This will contain a ToastManager instance reference so that toasts
// can be created from anywhere without having to store toasts state
// inside MobX or having to do weird findDOMNode stuff.
let manager: ToastManager = null;
const generateRandomId = () => Math.floor(Math.random() * 0x10000).toString(16);
interface IState {
notifications: INotification[];
}
export class ToastManager extends React.Component<{}, IState> {
public state: IState = {
notifications: [],
};
public componentDidMount() {
manager = this;
}
public addNotification(message: string, type: NotificationType) {
const { notifications } = this.state;
const notification = {
id: generateRandomId(),
message,
type,
};
this.setState({
notifications: [notification, ...notifications],
});
}
public onRemoveNotification = (id: string) => {
this.setState({
notifications: reject(this.state.notifications, { id }),
});
};
public render() {
const { notifications } = this.state;
return (
<div className="z-50 fixed top-0 inset-x-0 flex flex-col items-center pointer-events-none">
{notifications.map(notification => (
<Toast
key={notification.id}
onRemove={this.onRemoveNotification}
{...notification}
/>
))}
</div>
);
}
}
/**
* Syntax sugar allowing to safely inject a toast inside the ToastManager singleton
* @param {*} message
* @param {*} type
*/
export function toast(message: string, type: NotificationType) {
if (!manager) {
return;
}
manager.addNotification(message, type);
}
================================================
FILE: src/components/ToastManager/index.ts
================================================
export { ToastManager, toast } from './ToastManager';
================================================
FILE: src/components/ToastManager/types.ts
================================================
export interface INotification {
id: string;
message: string;
type: NotificationType;
}
export type NotificationType = 'info' | 'error';
================================================
FILE: src/components/index.ts
================================================
export { Dropdown } from './Dropdown';
export { Loader } from './Loader';
export { Header } from './Header';
export { FilterLink } from './FilterLink';
export { ToastManager } from './ToastManager';
================================================
FILE: src/constants/darkMode.ts
================================================
export const DARK_MODE = {
DISABLED: 'DISABLED',
ENABLED: 'ENABLED',
AT_NIGHT: 'AT_NIGHT',
};
================================================
FILE: src/constants/dates.ts
================================================
import { find } from 'lodash';
import moment from 'moment';
export type DateType =
| 'last_week'
| 'last_two_weeks'
| 'last_month'
| 'last_six_months'
| 'last_year'
| 'anytime';
interface IDatePreset {
name: string;
value: DateType;
data: {
amount: any;
unit: any;
};
}
export const DATES: IDatePreset[] = [
{ name: 'Last week', value: 'last_week', data: { amount: 1, unit: 'week' } },
{
name: 'Last 2 weeks',
value: 'last_two_weeks',
data: { amount: 2, unit: 'weeks' },
},
{
name: 'Last month',
value: 'last_month',
data: { amount: 1, unit: 'month' },
},
{
name: 'Last 6 months',
value: 'last_six_months',
data: { amount: 6, unit: 'months' },
},
{ name: 'Last year', value: 'last_year', data: { amount: 1, unit: 'year' } },
{ name: 'Anytime', value: 'anytime', data: { amount: 10, unit: 'years' } },
];
/**
* Returns a formatted date as used in a Github filter from
* the value of the date object
*/
export const getDateFromValue = (value: DateType) => {
const {
data: { amount, unit },
} = find(DATES, { value });
return moment()
.subtract(amount, unit)
.format('YYYY-MM-DD');
};
================================================
FILE: src/constants/languages.ts
================================================
export const LANGUAGES = [
{ value: null, name: 'All Languages' },
{ value: '1c-enterprise', name: '1C Enterprise' },
{ value: 'abap', name: 'ABAP' },
{ value: 'abnf', name: 'ABNF' },
{ value: 'actionscript', name: 'ActionScript' },
{ value: 'ada', name: 'Ada' },
{ value: 'adobe-font-metrics', name: 'Adobe Font Metrics' },
{ value: 'agda', name: 'Agda' },
{ value: 'ags-script', name: 'AGS Script' },
{ value: 'alloy', name: 'Alloy' },
{ value: 'alpine-abuild', name: 'Alpine Abuild' },
{ value: 'ampl', name: 'AMPL' },
{ value: 'angelscript', name: 'AngelScript' },
{ value: 'ant-build-system', name: 'Ant Build System' },
{ value: 'antlr', name: 'ANTLR' },
{ value: 'apacheconf', name: 'ApacheConf' },
{ value: 'apex', name: 'Apex' },
{ value: 'api-blueprint', name: 'API Blueprint' },
{ value: 'apl', name: 'APL' },
{ value: 'apollo-guidance-computer', name: 'Apollo Guidance Computer' },
{ value: 'applescript', name: 'AppleScript' },
{ value: 'arc', name: 'Arc' },
{ value: 'asciidoc', name: 'AsciiDoc' },
{ value: 'asn.1', name: 'ASN.1' },
{ value: 'asp', name: 'ASP' },
{ value: 'aspectj', name: 'AspectJ' },
{ value: 'assembly', name: 'Assembly' },
{ value: 'ats', name: 'ATS' },
{ value: 'augeas', name: 'Augeas' },
{ value: 'autohotkey', name: 'AutoHotkey' },
{ value: 'autoit', name: 'AutoIt' },
{ value: 'awk', name: 'Awk' },
{ value: 'ballerina', name: 'Ballerina' },
{ value: 'batchfile', name: 'Batchfile' },
{ value: 'befunge', name: 'Befunge' },
{ value: 'bison', name: 'Bison' },
{ value: 'bitbake', name: 'BitBake' },
{ value: 'blade', name: 'Blade' },
{ value: 'blitzbasic', name: 'BlitzBasic' },
{ value: 'blitzmax', name: 'BlitzMax' },
{ value: 'bluespec', name: 'Bluespec' },
{ value: 'boo', name: 'Boo' },
{ value: 'brainfuck', name: 'Brainfuck' },
{ value: 'brightscript', name: 'Brightscript' },
{ value: 'bro', name: 'Bro' },
{ value: 'c', name: 'C' },
{ value: 'c%23', name: 'C#' },
{ value: 'c++', name: 'C++' },
{ value: 'c-objdump', name: 'C-ObjDump' },
{ value: 'c2hs-haskell', name: 'C2hs Haskell' },
{ value: "cap'n-proto", name: "Cap'n Proto" },
{ value: 'cartocss', name: 'CartoCSS' },
{ value: 'ceylon', name: 'Ceylon' },
{ value: 'chapel', name: 'Chapel' },
{ value: 'charity', name: 'Charity' },
{ value: 'chuck', name: 'ChucK' },
{ value: 'cirru', name: 'Cirru' },
{ value: 'clarion', name: 'Clarion' },
{ value: 'clean', name: 'Clean' },
{ value: 'click', name: 'Click' },
{ value: 'clips', name: 'CLIPS' },
{ value: 'clojure', name: 'Clojure' },
{ value: 'closure-templates', name: 'Closure Templates' },
{
value: 'cloud-firestore-security-rules',
name: 'Cloud Firestore Security Rules',
},
{ value: 'cmake', name: 'CMake' },
{ value: 'cobol', name: 'COBOL' },
{ value: 'coffeescript', name: 'CoffeeScript' },
{ value: 'coldfusion', name: 'ColdFusion' },
{ value: 'coldfusion-cfc', name: 'ColdFusion CFC' },
{ value: 'collada', name: 'COLLADA' },
{ value: 'common-lisp', name: 'Common Lisp' },
{ value: 'common-workflow-language', name: 'Common Workflow Language' },
{ value: 'component-pascal', name: 'Component Pascal' },
{ value: 'conll-u', name: 'CoNLL-U' },
{ value: 'cool', name: 'Cool' },
{ value: 'coq', name: 'Coq' },
{ value: 'cpp-objdump', name: 'Cpp-ObjDump' },
{ value: 'creole', name: 'Creole' },
{ value: 'crystal', name: 'Crystal' },
{ value: 'cson', name: 'CSON' },
{ value: 'csound', name: 'Csound' },
{ value: 'csound-document', name: 'Csound Document' },
{ value: 'csound-score', name: 'Csound Score' },
{ value: 'css', name: 'CSS' },
{ value: 'csv', name: 'CSV' },
{ value: 'cuda', name: 'Cuda' },
{ value: 'cweb', name: 'CWeb' },
{ value: 'cycript', name: 'Cycript' },
{ value: 'cython', name: 'Cython' },
{ value: 'd', name: 'D' },
{ value: 'd-objdump', name: 'D-ObjDump' },
{ value: 'darcs-patch', name: 'Darcs Patch' },
{ value: 'dart', name: 'Dart' },
{ value: 'dataweave', name: 'DataWeave' },
{ value: 'desktop', name: 'desktop' },
{ value: 'diff', name: 'Diff' },
{ value: 'digital-command-language', name: 'DIGITAL Command Language' },
{ value: 'dm', name: 'DM' },
{ value: 'dns-zone', name: 'DNS Zone' },
{ value: 'dockerfile', name: 'Dockerfile' },
{ value: 'dogescript', name: 'Dogescript' },
{ value: 'dtrace', name: 'DTrace' },
{ value: 'dylan', name: 'Dylan' },
{ value: 'e', name: 'E' },
{ value: 'eagle', name: 'Eagle' },
{ value: 'easybuild', name: 'Easybuild' },
{ value: 'ebnf', name: 'EBNF' },
{ value: 'ec', name: 'eC' },
{ value: 'ecere-projects', name: 'Ecere Projects' },
{ value: 'ecl', name: 'ECL' },
{ value: 'eclipse', name: 'ECLiPSe' },
{ value: 'edje-data-collection', name: 'Edje Data Collection' },
{ value: 'edn', name: 'edn' },
{ value: 'eiffel', name: 'Eiffel' },
{ value: 'ejs', name: 'EJS' },
{ value: 'elixir', name: 'Elixir' },
{ value: 'elm', name: 'Elm' },
{ value: 'emacs-lisp', name: 'Emacs Lisp' },
{ value: 'emberscript', name: 'EmberScript' },
{ value: 'eq', name: 'EQ' },
{ value: 'erlang', name: 'Erlang' },
{ value: 'f%23', name: 'F#' },
{ value: 'factor', name: 'Factor' },
{ value: 'fancy', name: 'Fancy' },
{ value: 'fantom', name: 'Fantom' },
{ value: 'filebench-wml', name: 'Filebench WML' },
{ value: 'filterscript', name: 'Filterscript' },
{ value: 'fish', name: 'fish' },
{ value: 'flux', name: 'FLUX' },
{ value: 'formatted', name: 'Formatted' },
{ value: 'forth', name: 'Forth' },
{ value: 'fortran', name: 'Fortran' },
{ value: 'freemarker', name: 'FreeMarker' },
{ value: 'frege', name: 'Frege' },
{ value: 'g-code', name: 'G-code' },
{ value: 'game-maker-language', name: 'Game Maker Language' },
{ value: 'gams', name: 'GAMS' },
{ value: 'gap', name: 'GAP' },
{ value: 'gcc-machine-description', name: 'GCC Machine Description' },
{ value: 'gdb', name: 'GDB' },
{ value: 'gdscript', name: 'GDScript' },
{ value: 'genie', name: 'Genie' },
{ value: 'genshi', name: 'Genshi' },
{ value: 'gentoo-ebuild', name: 'Gentoo Ebuild' },
{ value: 'gentoo-eclass', name: 'Gentoo Eclass' },
{ value: 'gerber-image', name: 'Gerber Image' },
{ value: 'gettext-catalog', name: 'Gettext Catalog' },
{ value: 'gherkin', name: 'Gherkin' },
{ value: 'glsl', name: 'GLSL' },
{ value: 'glyph', name: 'Glyph' },
{ value: 'gn', name: 'GN' },
{ value: 'gnuplot', name: 'Gnuplot' },
{ value: 'go', name: 'Go' },
{ value: 'golo', name: 'Golo' },
{ value: 'gosu', name: 'Gosu' },
{ value: 'grace', name: 'Grace' },
{ value: 'gradle', name: 'Gradle' },
{ value: 'grammatical-framework', name: 'Grammatical Framework' },
{ value: 'graph-modeling-language', name: 'Graph Modeling Language' },
{ value: 'graphql', name: 'GraphQL' },
{ value: 'graphviz-(dot)', name: 'Graphviz (DOT)' },
{ value: 'groovy', name: 'Groovy' },
{ value: 'groovy-server-pages', name: 'Groovy Server Pages' },
{ value: 'hack', name: 'Hack' },
{ value: 'haml', name: 'Haml' },
{ value: 'handlebars', name: 'Handlebars' },
{ value: 'harbour', name: 'Harbour' },
{ value: 'haskell', name: 'Haskell' },
{ value: 'haxe', name: 'Haxe' },
{ value: 'hcl', name: 'HCL' },
{ value: 'hiveql', name: 'HiveQL' },
{ value: 'hlsl', name: 'HLSL' },
{ value: 'html', name: 'HTML' },
{ value: 'html+django', name: 'HTML+Django' },
{ value: 'html+ecr', name: 'HTML+ECR' },
{ value: 'html+eex', name: 'HTML+EEX' },
{ value: 'html+erb', name: 'HTML+ERB' },
{ value: 'html+php', name: 'HTML+PHP' },
{ value: 'http', name: 'HTTP' },
{ value: 'hxml', name: 'HXML' },
{ value: 'hy', name: 'Hy' },
{ value: 'hyphy', name: 'HyPhy' },
{ value: 'idl', name: 'IDL' },
{ value: 'idris', name: 'Idris' },
{ value: 'igor-pro', name: 'IGOR Pro' },
{ value: 'inform-7', name: 'Inform 7' },
{ value: 'ini', name: 'INI' },
{ value: 'inno-setup', name: 'Inno Setup' },
{ value: 'io', name: 'Io' },
{ value: 'ioke', name: 'Ioke' },
{ value: 'irc-log', name: 'IRC log' },
{ value: 'isabelle', name: 'Isabelle' },
{ value: 'isabelle-root', name: 'Isabelle ROOT' },
{ value: 'j', name: 'J' },
{ value: 'jasmin', name: 'Jasmin' },
{ value: 'java', name: 'Java' },
{ value: 'java-server-pages', name: 'Java Server Pages' },
{ value: 'javascript', name: 'JavaScript' },
{ value: 'jflex', name: 'JFlex' },
{ value: 'jison', name: 'Jison' },
{ value: 'jison-lex', name: 'Jison Lex' },
{ value: 'jolie', name: 'Jolie' },
{ value: 'json', name: 'JSON' },
{ value: 'json-with-comments', name: 'JSON with Comments' },
{ value: 'json5', name: 'JSON5' },
{ value: 'jsoniq', name: 'JSONiq' },
{ value: 'jsonld', name: 'JSONLD' },
{ value: 'jsx', name: 'JSX' },
{ value: 'julia', name: 'Julia' },
{ value: 'jupyter-notebook', name: 'Jupyter Notebook' },
{ value: 'kicad-layout', name: 'KiCad Layout' },
{ value: 'kicad-legacy-layout', name: 'KiCad Legacy Layout' },
{ value: 'kicad-schematic', name: 'KiCad Schematic' },
{ value: 'kit', name: 'Kit' },
{ value: 'kotlin', name: 'Kotlin' },
{ value: 'krl', name: 'KRL' },
{ value: 'labview', name: 'LabVIEW' },
{ value: 'lasso', name: 'Lasso' },
{ value: 'latte', name: 'Latte' },
{ value: 'lean', name: 'Lean' },
{ value: 'less', name: 'Less' },
{ value: 'lex', name: 'Lex' },
{ value: 'lfe', name: 'LFE' },
{ value: 'lilypond', name: 'LilyPond' },
{ value: 'limbo', name: 'Limbo' },
{ value: 'linker-script', name: 'Linker Script' },
{ value: 'linux-kernel-module', name: 'Linux Kernel Module' },
{ value: 'liquid', name: 'Liquid' },
{ value: 'literate-agda', name: 'Literate Agda' },
{ value: 'literate-coffeescript', name: 'Literate CoffeeScript' },
{ value: 'literate-haskell', name: 'Literate Haskell' },
{ value: 'livescript', name: 'LiveScript' },
{ value: 'llvm', name: 'LLVM' },
{ value: 'logos', name: 'Logos' },
{ value: 'logtalk', name: 'Logtalk' },
{ value: 'lolcode', name: 'LOLCODE' },
{ value: 'lookml', name: 'LookML' },
{ value: 'loomscript', name: 'LoomScript' },
{ value: 'lsl', name: 'LSL' },
{ value: 'lua', name: 'Lua' },
{ value: 'm', name: 'M' },
{ value: 'm4', name: 'M4' },
{ value: 'm4sugar', name: 'M4Sugar' },
{ value: 'makefile', name: 'Makefile' },
{ value: 'mako', name: 'Mako' },
{ value: 'markdown', name: 'Markdown' },
{ value: 'marko', name: 'Marko' },
{ value: 'mask', name: 'Mask' },
{ value: 'mathematica', name: 'Mathematica' },
{ value: 'matlab', name: 'Matlab' },
{ value: 'maven-pom', name: 'Maven POM' },
{ value: 'max', name: 'Max' },
{ value: 'maxscript', name: 'MAXScript' },
{ value: 'mediawiki', name: 'MediaWiki' },
{ value: 'mercury', name: 'Mercury' },
{ value: 'meson', name: 'Meson' },
{ value: 'metal', name: 'Metal' },
{ value: 'minid', name: 'MiniD' },
{ value: 'mirah', name: 'Mirah' },
{ value: 'modelica', name: 'Modelica' },
{ value: 'modula-2', name: 'Modula-2' },
{ value: 'modula-3', name: 'Modula-3' },
{ value: 'module-management-system', name: 'Module Management System' },
{ value: 'monkey', name: 'Monkey' },
{ value: 'moocode', name: 'Moocode' },
{ value: 'moonscript', name: 'MoonScript' },
{ value: 'mql4', name: 'MQL4' },
{ value: 'mql5', name: 'MQL5' },
{ value: 'mtml', name: 'MTML' },
{ value: 'muf', name: 'MUF' },
{ value: 'mupad', name: 'mupad' },
{ value: 'myghty', name: 'Myghty' },
{ value: 'ncl', name: 'NCL' },
{ value: 'nearley', name: 'Nearley' },
{ value: 'nemerle', name: 'Nemerle' },
{ value: 'nesc', name: 'nesC' },
{ value: 'netlinx', name: 'NetLinx' },
{ value: 'netlinx+erb', name: 'NetLinx+ERB' },
{ value: 'netlogo', name: 'NetLogo' },
{ value: 'newlisp', name: 'NewLisp' },
{ value: 'nextflow', name: 'Nextflow' },
{ value: 'nginx', name: 'Nginx' },
{ value: 'nim', name: 'Nim' },
{ value: 'ninja', name: 'Ninja' },
{ value: 'nit', name: 'Nit' },
{ value: 'nix', name: 'Nix' },
{ value: 'nl', name: 'NL' },
{ value: 'nsis', name: 'NSIS' },
{ value: 'nu', name: 'Nu' },
{ value: 'numpy', name: 'NumPy' },
{ value: 'objdump', name: 'ObjDump' },
{ value: 'objective-c', name: 'Objective-C' },
{ value: 'objective-c++', name: 'Objective-C++' },
{ value: 'objective-j', name: 'Objective-J' },
{ value: 'ocaml', name: 'OCaml' },
{ value: 'omgrofl', name: 'Omgrofl' },
{ value: 'ooc', name: 'ooc' },
{ value: 'opa', name: 'Opa' },
{ value: 'opal', name: 'Opal' },
{ value: 'opencl', name: 'OpenCL' },
{ value: 'openedge-abl', name: 'OpenEdge ABL' },
{ value: 'openrc-runscript', name: 'OpenRC runscript' },
{ value: 'openscad', name: 'OpenSCAD' },
{ value: 'opentype-feature-file', name: 'OpenType Feature File' },
{ value: 'org', name: 'Org' },
{ value: 'ox', name: 'Ox' },
{ value: 'oxygene', name: 'Oxygene' },
{ value: 'oz', name: 'Oz' },
{ value: 'p4', name: 'P4' },
{ value: 'pan', name: 'Pan' },
{ value: 'papyrus', name: 'Papyrus' },
{ value: 'parrot', name: 'Parrot' },
{ value: 'parrot-assembly', name: 'Parrot Assembly' },
{
value: 'parrot-internal-representation',
name: 'Parrot Internal Representation',
},
{ value: 'pascal', name: 'Pascal' },
{ value: 'pawn', name: 'PAWN' },
{ value: 'pep8', name: 'Pep8' },
{ value: 'perl', name: 'Perl' },
{ value: 'perl-6', name: 'Perl 6' },
{ value: 'php', name: 'PHP' },
{ value: 'pic', name: 'Pic' },
{ value: 'pickle', name: 'Pickle' },
{ value: 'picolisp', name: 'PicoLisp' },
{ value: 'piglatin', name: 'PigLatin' },
{ value: 'pike', name: 'Pike' },
{ value: 'plpgsql', name: 'PLpgSQL' },
{ value: 'plsql', name: 'PLSQL' },
{ value: 'pod', name: 'Pod' },
{ value: 'pogoscript', name: 'PogoScript' },
{ value: 'pony', name: 'Pony' },
{ value: 'postcss', name: 'PostCSS' },
{ value: 'postscript', name: 'PostScript' },
{ value: 'pov-ray-sdl', name: 'POV-Ray SDL' },
{ value: 'powerbuilder', name: 'PowerBuilder' },
{ value: 'powershell', name: 'PowerShell' },
{ value: 'processing', name: 'Processing' },
{ value: 'prolog', name: 'Prolog' },
{ value: 'propeller-spin', name: 'Propeller Spin' },
{ value: 'protocol-buffer', name: 'Protocol Buffer' },
{ value: 'public-key', name: 'Public Key' },
{ value: 'pug', name: 'Pug' },
{ value: 'puppet', name: 'Puppet' },
{ value: 'pure-data', name: 'Pure Data' },
{ value: 'purebasic', name: 'PureBasic' },
{ value: 'purescript', name: 'PureScript' },
{ value: 'python', name: 'Python' },
{ value: 'python-console', name: 'Python console' },
{ value: 'python-traceback', name: 'Python traceback' },
{ value: 'q', name: 'q' },
{ value: 'qmake', name: 'QMake' },
{ value: 'qml', name: 'QML' },
{ value: 'r', name: 'R' },
{ value: 'racket', name: 'Racket' },
{ value: 'ragel', name: 'Ragel' },
{ value: 'raml', name: 'RAML' },
{ value: 'rascal', name: 'Rascal' },
{ value: 'raw-token-data', name: 'Raw token data' },
{ value: 'rdoc', name: 'RDoc' },
{ value: 'realbasic', name: 'REALbasic' },
{ value: 'reason', name: 'Reason' },
{ value: 'rebol', name: 'Rebol' },
{ value: 'red', name: 'Red' },
{ value: 'redcode', name: 'Redcode' },
{ value: 'regular-expression', name: 'Regular Expression' },
{ value: "ren'py", name: "Ren'Py" },
{ value: 'renderscript', name: 'RenderScript' },
{ value: 'restructuredtext', name: 'reStructuredText' },
{ value: 'rexx', name: 'REXX' },
{ value: 'rhtml', name: 'RHTML' },
{ value: 'ring', name: 'Ring' },
{ value: 'rmarkdown', name: 'RMarkdown' },
{ value: 'robotframework', name: 'RobotFramework' },
{ value: 'roff', name: 'Roff' },
{ value: 'rouge', name: 'Rouge' },
{ value: 'rpc', name: 'RPC' },
{ value: 'rpm-spec', name: 'RPM Spec' },
{ value: 'ruby', name: 'Ruby' },
{ value: 'runoff', name: 'RUNOFF' },
{ value: 'rust', name: 'Rust' },
{ value: 'sage', name: 'Sage' },
{ value: 'saltstack', name: 'SaltStack' },
{ value: 'sas', name: 'SAS' },
{ value: 'sass', name: 'Sass' },
{ value: 'scala', name: 'Scala' },
{ value: 'scaml', name: 'Scaml' },
{ value: 'scheme', name: 'Scheme' },
{ value: 'scilab', name: 'Scilab' },
{ value: 'scss', name: 'SCSS' },
{ value: 'sed', name: 'sed' },
{ value: 'self', name: 'Self' },
{ value: 'shaderlab', name: 'ShaderLab' },
{ value: 'shell', name: 'Shell' },
{ value: 'shellsession', name: 'ShellSession' },
{ value: 'shen', name: 'Shen' },
{ value: 'slash', name: 'Slash' },
{ value: 'slim', name: 'Slim' },
{ value: 'smali', name: 'Smali' },
{ value: 'smalltalk', name: 'Smalltalk' },
{ value: 'smarty', name: 'Smarty' },
{ value: 'smt', name: 'SMT' },
{ value: 'solidity', name: 'Solidity' },
{ value: 'sourcepawn', name: 'SourcePawn' },
{ value: 'sparql', name: 'SPARQL' },
{ value: 'spline-font-database', name: 'Spline Font Database' },
{ value: 'sqf', name: 'SQF' },
{ value: 'sql', name: 'SQL' },
{ value: 'sqlpl', name: 'SQLPL' },
{ value: 'squirrel', name: 'Squirrel' },
{ value: 'srecode-template', name: 'SRecode Template' },
{ value: 'stan', name: 'Stan' },
{ value: 'standard-ml', name: 'Standard ML' },
{ value: 'stata', name: 'Stata' },
{ value: 'ston', name: 'STON' },
{ value: 'stylus', name: 'Stylus' },
{ value: 'subrip-text', name: 'SubRip Text' },
{ value: 'sugarss', name: 'SugarSS' },
{ value: 'supercollider', name: 'SuperCollider' },
{ value: 'svg', name: 'SVG' },
{ value: 'swift', name: 'Swift' },
{ value: 'systemverilog', name: 'SystemVerilog' },
{ value: 'tcl', name: 'Tcl' },
{ value: 'tcsh', name: 'Tcsh' },
{ value: 'tea', name: 'Tea' },
{ value: 'terra', name: 'Terra' },
{ value: 'tex', name: 'TeX' },
{ value: 'text', name: 'Text' },
{ value: 'textile', name: 'Textile' },
{ value: 'thrift', name: 'Thrift' },
{ value: 'ti-program', name: 'TI Program' },
{ value: 'tla', name: 'TLA' },
{ value: 'toml', name: 'TOML' },
{ value: 'turing', name: 'Turing' },
{ value: 'turtle', name: 'Turtle' },
{ value: 'twig', name: 'Twig' },
{ value: 'txl', name: 'TXL' },
{ value: 'type-language', name: 'Type Language' },
{ value: 'typescript', name: 'TypeScript' },
{ value: 'unified-parallel-c', name: 'Unified Parallel C' },
{ value: 'unity3d-asset', name: 'Unity3D Asset' },
{ value: 'unix-assembly', name: 'Unix Assembly' },
{ value: 'uno', name: 'Uno' },
{ value: 'unrealscript', name: 'UnrealScript' },
{ value: 'urweb', name: 'UrWeb' },
{ value: 'vala', name: 'Vala' },
{ value: 'vcl', name: 'VCL' },
{ value: 'verilog', name: 'Verilog' },
{ value: 'vhdl', name: 'VHDL' },
{ value: 'vim-script', name: 'Vim script' },
{ value: 'visual-basic', name: 'Visual Basic' },
{ value: 'volt', name: 'Volt' },
{ value: 'vue', name: 'Vue' },
{ value: 'wavefront-material', name: 'Wavefront Material' },
{ value: 'wavefront-object', name: 'Wavefront Object' },
{ value: 'wdl', name: 'wdl' },
{ value: 'web-ontology-language', name: 'Web Ontology Language' },
{ value: 'webassembly', name: 'WebAssembly' },
{ value: 'webidl', name: 'WebIDL' },
{ value: 'wisp', name: 'wisp' },
{
value: 'world-of-warcraft-addon-data',
name: 'World of Warcraft Addon Data',
},
{ value: 'x-bitmap', name: 'X BitMap' },
{ value: 'x-pixmap', name: 'X PixMap' },
{ value: 'x10', name: 'X10' },
{ value: 'xbase', name: 'xBase' },
{ value: 'xc', name: 'XC' },
{ value: 'xcompose', name: 'XCompose' },
{ value: 'xml', name: 'XML' },
{ value: 'xojo', name: 'Xojo' },
{ value: 'xpages', name: 'XPages' },
{ value: 'xproc', name: 'XProc' },
{ value: 'xquery', name: 'XQuery' },
{ value: 'xs', name: 'XS' },
{ value: 'xslt', name: 'XSLT' },
{ value: 'xtend', name: 'Xtend' },
{ value: 'yacc', name: 'Yacc' },
{ value: 'yaml', name: 'YAML' },
{ value: 'yang', name: 'YANG' },
{ value: 'yara', name: 'YARA' },
{ value: 'zephir', name: 'Zephir' },
{ value: 'zimpl', name: 'Zimpl' },
];
================================================
FILE: src/containers/FilterEditModal/FilterEditModal.tsx
================================================
import { pick } from 'lodash';
import { toJS } from 'mobx';
import { inject, observer } from 'mobx-react';
import React, { useState } from 'react';
import { compose } from 'recompose';
import styled from 'styled-components';
import { Modal } from '../../components/Modal';
import { providers, ProviderType } from '../../providers';
import { Filter, FiltersStore } from '../../store/filters';
import { PredicatesStep } from './PredicatesStep';
import { ProviderStep } from './ProviderStep';
const Container = styled.div`
width: 650px;
`;
enum STEPS {
PROVIDERS,
PREDICATES,
}
interface IProps {
initialFilter?: Filter;
onClose: () => void;
}
interface IInnerProps extends IProps {
filtersStore: FiltersStore;
}
export const FilterEditModal = compose<IInnerProps, IProps>(
inject('filtersStore'),
observer
)(({ initialFilter, onClose, filtersStore }) => {
const [step, setStep] = useState(
initialFilter ? STEPS.PREDICATES : STEPS.PROVIDERS
);
const defaultedFilter = defaultFilter(initialFilter);
const [provider, setProvider] = useState(defaultedFilter.provider);
const [label, setLabel] = useState(defaultedFilter.label);
const [predicates, setPredicates] = useState(defaultedFilter.predicates);
function handleSave() {
filtersStore.saveFilter({
id: defaultedFilter.id,
provider,
label,
predicates,
});
onClose();
}
return (
<Modal onClose={onClose}>
<Container className="mt-32 mb-16 mx-auto">
{step === STEPS.PROVIDERS && (
<ProviderStep
provider={provider}
onChange={setProvider}
previous={onClose}
next={() => setStep(STEPS.PREDICATES)}
/>
)}
{step === STEPS.PREDICATES && (
<PredicatesStep
label={label}
setLabel={setLabel}
predicates={predicates}
setPredicates={setPredicates}
provider={providers[provider]}
previous={onClose}
next={handleSave}
/>
)}
</Container>
</Modal>
);
});
function defaultFilter(filter?: Filter) {
if (!filter) {
return {
id: undefined,
provider: ProviderType.GITHUB,
label: 'Unnamed filter',
predicates: [],
};
}
return pick(toJS(filter), ['id', 'provider', 'label', 'predicates']);
}
================================================
FILE: src/containers/FilterEditModal/PredicatesStep.tsx
================================================
import cx from 'classnames';
import { chain, get } from 'lodash';
import { inject, observer } from 'mobx-react';
import React, { ChangeEvent, useEffect } from 'react';
import { compose } from 'recompose';
import { Button, ButtonType } from '../../components/Button';
import { FilterPredicate } from '../../components/FilterPredicate';
import { AbstractProvider, IStoredPredicate } from '../../providers';
import { SettingsStore } from '../../store/settings';
interface IProps {
label: string;
predicates: any[]; // TODO
provider: AbstractProvider;
setLabel: (name: string) => void;
setPredicates: (predicates: any[]) => void; // TODO
previous: () => void;
next: () => void;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const PredicatesStep = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(
({
label,
predicates,
provider,
setLabel,
setPredicates,
previous,
next,
settingsStore,
}) => {
// Save on Enter
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter') {
next();
}
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [next]);
/**
* Add a new predicate to the list of predicates
*/
function handleAddPredicate(event: ChangeEvent<HTMLSelectElement>) {
const predicate = provider.findPredicate(event.target.value);
if (!predicate) {
return;
}
setPredicates([
...predicates,
{
type: predicate.name,
operator: get(predicate, 'operators.0.value'),
value: '',
},
]);
}
/**
* Update a predicate in place
* @param index Index at which the predicate is located
*/
const handlePredicateChange = (index: number) => ({
value,
operator,
}: IStoredPredicate) => {
setPredicates([
...predicates.slice(0, index),
{ ...predicates[index], value, operator },
...predicates.slice(index + 1),
]);
};
/**
* Remove a predicate
* @param index Index at which the predicate is located
*/
const handlePredicateDeletion = (index: number) => () => {
setPredicates([
...predicates.slice(0, index),
...predicates.slice(index + 1),
]);
};
return (
<div>
<div className="text-base text-gray-500 font-medium tracking-wider">
Filter name
</div>
<input
type="text"
value={label}
className={cx(
'w-full text-2xl bg-transparent outline-none',
settingsStore.isDark ? 'text-white' : 'text-black'
)}
onChange={event => setLabel(event.target.value)}
data-id="filter-label-input"
/>
<div className="text-base text-gray-500 font-medium tracking-wider mt-8 mb-2">
Predicates
</div>
<div>
{predicates.map((predicate, index) => (
<FilterPredicate
key={index}
{...predicate}
provider={provider}
onChange={handlePredicateChange(index)}
onDelete={handlePredicateDeletion(index)}
/>
))}
</div>
<div>
<select
value=""
onChange={handleAddPredicate}
className={cx(
'bg-transparent appearance-none border-none outline-none cursor-pointer',
settingsStore.isDark ? 'text-gray-500' : 'text-gray-900'
)}
data-id="add-predicate-dropdown"
>
<option key="__default" value="">
+ Add a predicate
</option>
{chain(provider.getAvailablePredicates())
.orderBy('label')
.map(({ name, label: predicateLabel }) => (
<option key={name} value={name}>
{predicateLabel}
</option>
))
.value()}
</select>
</div>
<div className="flex justify-end mt-10">
<Button onClick={previous} className="mr-3">
Cancel
</Button>
<Button onClick={next} type={ButtonType.PRIMARY}>
Continue
</Button>
</div>
</div>
);
}
);
================================================
FILE: src/containers/FilterEditModal/ProviderStep.tsx
================================================
import React from 'react';
import { Button, ButtonType } from '../../components/Button';
import { RadioCard } from '../../components/RadioCard';
import { ProviderType } from '../../providers';
interface IProps {
provider: ProviderType;
onChange: (provider: ProviderType) => void;
previous: () => void;
next: () => void;
}
export const ProviderStep = ({
provider,
onChange,
previous,
next,
}: IProps) => {
return (
<div className="flex-1 flex flex-col items-stretch">
<div className="font-medium mb-4">
Where do you want to get data from?
</div>
<RadioCard
title="GitHub"
text="Filter issues and pull requests from public and private repositories."
icon="fab fa-github"
selected={provider === ProviderType.GITHUB}
onClick={() => onChange(ProviderType.GITHUB)}
/>
<RadioCard
title="Jira"
text="Filter and retrieve tickets from any Jira organization you may have access to."
icon="fab fa-jira"
selected={provider === ProviderType.JIRA}
onClick={() => onChange(ProviderType.JIRA)}
/>
<div className="flex justify-end mt-10">
<Button onClick={previous} className="mr-3">
Cancel
</Button>
<Button onClick={next} type={ButtonType.PRIMARY}>
Continue
</Button>
</div>
</div>
);
};
================================================
FILE: src/containers/FilterEditModal/index.ts
================================================
export { FilterEditModal } from './FilterEditModal';
================================================
FILE: src/containers/RepoCard/RepoCard.tsx
================================================
/* eslint-disable @typescript-eslint/camelcase */
import cx from 'classnames';
import humanFormat from 'human-format';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import { SettingsStore } from '../../store/settings';
interface IProps {
repo: any;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const RepoCard = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ repo, settingsStore }) => {
const {
name,
description,
stargazers_count,
forks_count,
open_issues_count,
html_url,
language,
owner: { avatar_url, html_url: profile_url },
} = repo;
return (
<div className="w-1/3 pl-6 mb-6" data-id="repo-card">
<div
className={cx(
'h-64 flex flex-col px-5 py-4 shadow-lg rounded-lg',
settingsStore.isDark
? 'bg-gray-800 text-white'
: 'bg-white text-gray-900'
)}
>
<div className="flex items-center min-h-10">
<a
href={profile_url}
className="inline-block h-8 w-8 overflow-hidden rounded-full mr-3"
>
<img src={avatar_url} />
</a>
<a
className={cx(
'min-w-0 truncate hover:underline text-2xl py-2',
settingsStore.isDark
? 'text-blue-400'
: 'text-blue-500 hover:text-blue-600'
)}
href={html_url}
target="_blank"
rel="noopener noreferrer"
data-id="repo-link"
>
{name}
</a>
</div>
<div className="leading-normal mt-1 overflow-hidden">{description}</div>
<div
className={cx(
'flex-1 min-h-6 flex items-end mt-2',
settingsStore.isDark ? 'text-gray-500' : 'text-gray-700'
)}
>
{language && <div className="mr-5">{language}</div>}
<div className="mr-5">
<i className="fa fa-star" />{' '}
{humanFormat(stargazers_count, { decimals: 1, separator: '' })}
</div>
<div className="mr-5">
<i className="fa fa-code-branch" />{' '}
{humanFormat(forks_count, { decimals: 1, separator: '' })}
</div>
<div>
<i className="fa fa-exclamation-circle" />{' '}
{humanFormat(open_issues_count, { decimals: 1, separator: '' })}
</div>
</div>
</div>
</div>
);
});
================================================
FILE: src/containers/RepoCard/index.ts
================================================
export { RepoCard } from './RepoCard';
================================================
FILE: src/containers/SettingsModal/Panel.tsx
================================================
import { find } from 'lodash';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import { SettingsStore } from '../../store/settings';
import { SETTINGS_VIEWS } from './constants';
interface IProps {
selectedTab: string;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const Panel = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ selectedTab, settingsStore }) => {
const view = find(SETTINGS_VIEWS, { id: selectedTab });
if (!view) {
return null;
}
const Component = view.component;
return <Component settings={view.isProvider ? undefined : settingsStore} />;
});
================================================
FILE: src/containers/SettingsModal/SettingsModal.tsx
================================================
import React, { useState } from 'react';
import styled from 'styled-components';
import { Modal } from '../../components/Modal';
import { SETTINGS_VIEWS } from './constants';
import { Panel } from './Panel';
import { Sidebar } from './Sidebar';
const Container = styled.div`
width: 900px;
`;
interface IProps {
onClose: () => void;
}
export function SettingsModal({ onClose }: IProps) {
const [selectedTab, selectTab] = useState(SETTINGS_VIEWS[0].id);
return (
<Modal onClose={onClose}>
<Container className="mt-32 mx-auto flex">
<Sidebar selectedTab={selectedTab} selectTab={selectTab} />
<Panel selectedTab={selectedTab} />
</Container>
</Modal>
);
}
================================================
FILE: src/containers/SettingsModal/Sidebar.tsx
================================================
import cx from 'classnames';
import { partition } from 'lodash';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import styled from 'styled-components';
import { SettingsStore } from '../../store/settings';
import { SETTINGS_VIEWS } from './constants';
import { ISettingView } from './types';
const Wrapper = styled.div`
width: 200px;
font-family: Roboto;
padding-right: 30px;
flex-shrink: 0;
`;
const ItemHeader = styled.div`
font-size: 16px;
font-weight: 500;
letter-spacing: 0.1em;
padding-left: 12px;
margin-bottom: 14px;
`;
const Item = styled.div`
font-size: 18px;
padding: 6px;
padding-left: 12px;
margin-bottom: 10px;
cursor: pointer;
`;
interface IProps {
selectedTab: string;
selectTab: Function;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const Sidebar = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ selectedTab, selectTab, settingsStore }) => {
const [providerItems, staticItems] = partition(SETTINGS_VIEWS, 'isProvider');
function renderItems(items: ISettingView[]) {
return items.map(({ label, id }) => (
<Item
key={id}
onClick={() => selectTab(id)}
className={cx(
selectedTab === id && 'text-white bg-blue-500 font-medium rounded',
settingsStore.isDark && 'text-gray-300'
)}
data-setting-tab={label}
>
{label}
</Item>
));
}
const headerClass = settingsStore.isDark ? 'text-gray-600' : 'text-gray-500';
return (
<Wrapper>
<ItemHeader className={headerClass}>Settings</ItemHeader>
{renderItems(staticItems)}
<ItemHeader className={cx('mt-10', headerClass)}>Providers</ItemHeader>
{renderItems(providerItems)}
</Wrapper>
);
});
================================================
FILE: src/containers/SettingsModal/constants.ts
================================================
import { map } from 'lodash';
import { providers } from '../../providers';
import { CacheSettings, NightMode } from './tabs';
import { ISettingView } from './types';
export const SETTINGS_VIEWS: ISettingView[] = [
{
id: 'night_mode',
label: 'Night mode',
component: NightMode,
},
{
id: 'cache',
label: 'Cache',
component: CacheSettings,
},
...map(providers, ({ label, settingsComponent }, key) => ({
id: key,
label,
component: settingsComponent,
isProvider: true,
})),
];
================================================
FILE: src/containers/SettingsModal/index.ts
================================================
export { SettingsModal } from './SettingsModal';
================================================
FILE: src/containers/SettingsModal/tabs/Cache.tsx
================================================
import React from 'react';
import { Button, ButtonType } from '../../../components/Button';
import { toast } from '../../../components/ToastManager';
import { Cache } from '../../../lib/cache';
export const CacheSettings = () => {
function flushCache() {
Cache.flush();
toast('Cache was successfully cleared', 'info');
}
return (
<div>
<div className="font-medium mb-4">Cached data</div>
<p className="leading-normal">
In order to limit the amount of requests to the various providers, a lot
of them are actually cached. If for some reason you noticed
inconsistencies or outdated data, please file an issue.
</p>
<p className="mt-3">
In the meantime, you can clear the cached data to get back to a coherent
state.
</p>
<Button type={ButtonType.PRIMARY} onClick={flushCache} className="mt-4">
Clear cache
</Button>
</div>
);
};
================================================
FILE: src/containers/SettingsModal/tabs/NightMode.tsx
================================================
import React, { useEffect, useState } from 'react';
import { RadioCard } from '../../../components/RadioCard';
import { DARK_MODE } from '../../../constants/darkMode';
import { SettingsStore } from '../../../store/settings';
interface IProps {
settings: SettingsStore;
}
export const NightMode = ({ settings }: IProps) => {
const [darkMode, setDarkMode] = useState(settings.darkMode);
useEffect(() => settings.updateDarkMode(darkMode), [darkMode]);
return (
<div>
<div>
<div className="font-medium mb-4">
When should dark mode be enabled?
</div>
<RadioCard
title="Never"
text="The interface will stay bright, no matter what time of the day or night it is."
icon="fas fa-sun"
selected={darkMode === DARK_MODE.DISABLED}
onClick={() => setDarkMode(DARK_MODE.DISABLED)}
dark={settings.isDark}
/>
<RadioCard
title="Always"
text="The interface will always be dark, protecting your eyes from bright lights. It’s really useful at night."
icon="fas fa-moon"
selected={darkMode === DARK_MODE.ENABLED}
onClick={() => setDarkMode(DARK_MODE.ENABLED)}
dark={settings.isDark}
/>
<RadioCard
title="Only at night (from 7PM to 7AM)"
text="The perfect compromise between the two other options. Bright and legible during the day, but dark at night."
icon="fas fa-clock"
selected={darkMode === DARK_MODE.AT_NIGHT}
onClick={() => setDarkMode(DARK_MODE.AT_NIGHT)}
dark={settings.isDark}
/>
</div>
</div>
);
};
================================================
FILE: src/containers/SettingsModal/tabs/index.ts
================================================
export { NightMode } from './NightMode';
export { CacheSettings } from './Cache';
================================================
FILE: src/containers/SettingsModal/types.ts
================================================
export interface ISettingView {
id: string;
label: string;
component: any;
isProvider?: boolean;
}
================================================
FILE: src/containers/index.ts
================================================
export { RepoCard } from './RepoCard';
export { FilterEditModal } from './FilterEditModal';
export { SettingsModal } from './SettingsModal';
================================================
FILE: src/errors/InvalidCredentials.ts
================================================
import ExtendableError from 'es6-error';
export class InvalidCredentials extends ExtendableError {
constructor(message = 'Your token seems to be invalid') {
super(message);
}
}
================================================
FILE: src/errors/NeedTokenError.ts
================================================
import ExtendableError from 'es6-error';
export class NeedTokenError extends ExtendableError {
constructor(message = 'Fetching failed. Try to set a Github token') {
super(message);
}
}
================================================
FILE: src/errors/RateLimitError.ts
================================================
import ExtendableError from 'es6-error';
export class RateLimitError extends ExtendableError {
public remainingRateLimit: number;
constructor(remainingRateLimit: number) {
super('Rate limited. Set a Github token to raise the limit');
this.remainingRateLimit = Math.round(remainingRateLimit);
}
}
================================================
FILE: src/errors/index.ts
================================================
export { NeedTokenError } from './NeedTokenError';
export { RateLimitError } from './RateLimitError';
export { InvalidCredentials } from './InvalidCredentials';
================================================
FILE: src/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OctoLenses - Github Dashboard</title>
<link rel="stylesheet" href="./index.scss" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.0/css/all.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="container"></div>
<script type="module" src="index.tsx"></script>
</body>
</html>
================================================
FILE: src/index.scss
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css?family=Open+Sans|Roboto:400,500');
html {
line-height: 1.15;
}
body {
@apply text-gray-900 font-open;
@apply m-0 min-h-screen;
@apply bg-gray-100;
font-size: 100%;
&.dark {
@apply bg-gray-900 text-white;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-roboto;
}
a,
a:hover,
a:active,
a:visited {
@apply no-underline;
}
#container {
@apply min-h-full mx-auto;
@apply px-8;
max-width: 1180px;
.App {
@apply min-h-full flex flex-col;
}
}
.rtl {
direction: rtl;
}
================================================
FILE: src/index.tsx
================================================
import 'babel-polyfill';
import { Provider } from 'mobx-react';
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
import {
bootstrap,
filtersStore,
navigationStore,
settingsStore,
trendsStore,
} from './store';
bootstrap();
ReactDOM.render(
<Provider
navigationStore={navigationStore}
settingsStore={settingsStore}
trendsStore={trendsStore}
filtersStore={filtersStore}
>
<App />
</Provider>,
document.getElementById('container')
);
================================================
FILE: src/lib/assertUnreachable.ts
================================================
export function assertUnreachable<T>(_: never, defaultValue: T): T {
return defaultValue;
}
================================================
FILE: src/lib/cache.ts
================================================
import { chain, startsWith } from 'lodash';
interface ICacheEntry<T> {
expiresAt: number;
value: T;
}
export class Cache {
public static prefix = 'cache';
/**
* Remember the result of an expensive computation or data fetching, during a
* certain period. The result is identified by a unique key.
* @param key Key at which to store the result of the computation
* @param lifespan How long, in second, to store the item
* @param computer The method that computes the value, if not in the cache
*/
public static async remember<T = any>(
key: string,
lifespan: number,
computer: () => Promise<T>
): Promise<T> {
const cached = Cache.get<T>(key);
if (cached) {
if (Date.now() < cached.expiresAt) {
return cached.value;
}
Cache.forget(key);
}
const value = await computer();
Cache.put(key, value, lifespan);
return value;
}
/**
* Retrieve an item from the cache
* @param key Key at which the item is stored
*/
public static get<T = any>(key: string): ICacheEntry<T> {
const item = localStorage.getItem(Cache.getPrefixedKey(key));
try {
return JSON.parse(item);
} catch (error) {
// Do nothing
}
return null;
}
/**
* Put an item into the cache
* @param key Key at which to store the cached item
* @param value Item to cache
* @param lifespan How long, in seconds, we want to keep the item
*/
public static put(key: string, value: any, lifespan: number) {
localStorage.setItem(
Cache.getPrefixedKey(key),
JSON.stringify({
expiresAt: Date.now() + lifespan * 1000,
value,
})
);
}
/**
* Forget a cached item
* @param key Key at which the item is stored
*/
public static forget(key: string) {
localStorage.removeItem(Cache.getPrefixedKey(key));
}
/**
* Flush the whole cache
*/
public static flush() {
Cache.getCacheKeys().forEach(Cache.forget);
}
/**
* Flush all the expired entries from the cache
*/
public static flushExpired() {
Cache.getCacheKeys()
.filter(key => Cache.get(key).expiresAt < Date.now())
.forEach(Cache.forget);
}
/**
* Return a prefixed key
* @param key
*/
private static getPrefixedKey(key: string) {
if (Cache.isPrefixed(key)) {
return key;
}
return `${Cache.prefix}:${key}`;
}
/**
* Return whether a key is already prefixed or not
* @param key
*/
private static isPrefixed(key: string) {
return startsWith(key, `${Cache.prefix}:`);
}
/**
* Return all the keys stored in the cache
*/
private static getCacheKeys() {
const cachePrefix = `${Cache.prefix}:`;
return chain(localStorage)
.keys()
.filter(key => startsWith(key, cachePrefix))
.value();
}
}
================================================
FILE: src/lib/github/index.ts
================================================
export { fetchTrendingRepos } from './trending';
================================================
FILE: src/lib/github/trending/index.ts
================================================
import hash from 'object-hash';
import { Cache } from '../../../lib/cache';
import { client } from '../../../providers/github/fetchers/client';
interface IFetchTrendingReposParams {
language: string;
date: string;
token?: string;
}
/**
* Fetch the trending repositories from GitHub
* @param language What programming language the user is interested in
* @param date From which date
* @param token Github token of the user
*/
export const fetchTrendingRepos = async ({
language,
date,
token,
}: IFetchTrendingReposParams) => {
let query = `created:>${date}`;
if (language !== null) {
query += ` and language:${language}`;
}
const cacheKey = `github.trending.${hash(query)}`;
const { items: repos } = await Cache.remember(cacheKey, 60 * 60, () =>
client({
endpoint: '/search/repositories',
qs: `per_page=100&q=${query}&sort=stars&order=desc`,
token,
})
);
return repos;
};
================================================
FILE: src/migrations/index.ts
================================================
/* eslint-disable @typescript-eslint/camelcase */
import { IMigration } from './types';
import v0_to_v1 from './v0-to-v1';
import v1_to_v2 from './v1-to-v2';
import v2_to_v3 from './v2-to-v3';
import './testing-utils';
class Migrator {
private migrations: IMigration[] = [];
public migrate() {
console.log('[migration] Running necessary migrations...');
this.migrations.forEach(migration => {
if (migration.shouldRun()) {
console.log(`[migration] Running migration ${migration.name}`);
migration.run();
}
});
}
public registerMigration(migration: IMigration): Migrator {
this.migrations.push(migration);
return this;
}
}
export const migrator = new Migrator()
.registerMigration(new v0_to_v1())
.registerMigration(new v1_to_v2())
.registerMigration(new v2_to_v3());
================================================
FILE: src/migrations/mocks/index.ts
================================================
/* eslint-disable @typescript-eslint/camelcase */
import v0 from './v0';
import v1 from './v1';
import v1_withoutToken from './v1-without-token';
import v2 from './v2';
import v2_withNegatedPredicates from './v2-with-negated-predicates';
import v3_withDefaultedOperators from './v3-with-defaulted-operators';
const allMocks: Record<string, object> = {
v0,
v1,
v1_withoutToken,
v2,
v2_withNegatedPredicates,
v3_withDefaultedOperators,
};
export default allMocks;
================================================
FILE: src/migrations/mocks/v0.ts
================================================
export default {
filtersStore: {
data: [
{
id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',
label: 'OctoLenses Issues',
predicates: [
{ type: 'type', value: 'issues' },
{ type: 'repo', value: 'rgehan/octolenses', negated: false },
{ type: 'status', value: 'open' },
],
},
{
id: '92da29c0-5216-11e9-ad7a-73a635a52ed2',
label: 'My Private Filter',
predicates: [
{ type: 'status', value: 'open' },
{
type: 'repo',
value: 'botify-hq/botify-report',
negated: false,
},
{ type: 'author', value: 'rgehan', negated: false },
{ type: 'status', value: 'open', negated: false },
],
},
],
},
settingsStore: {
language: null,
dateRange: 'last_week',
token: '<token>',
wasOnboarded: true,
darkMode: 'DISABLED',
},
useNewTabPage: true,
};
================================================
FILE: src/migrations/mocks/v1-without-token.ts
================================================
export default {
filtersStore: {
data: [
{
id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',
label: 'OctoLenses Issues',
predicates: [
{ type: 'type', value: 'issues' },
{ type: 'repo', value: 'rgehan/octolenses', negated: false },
{ type: 'status', value: 'open' },
],
},
{
id: '92da29c0-5216-11e9-ad7a-73a635a52ed2',
label: 'My Private Filter',
predicates: [
{ type: 'status', value: 'open' },
{
type: 'repo',
value: 'botify-hq/botify-report',
negated: false,
},
{ type: 'author', value: 'rgehan', negated: false },
{ type: 'status', value: 'open', negated: false },
],
},
],
},
settingsStore: {
language: null,
dateRange: 'last_week',
wasOnboarded: true,
darkMode: 'DISABLED',
schemaVersion: 1,
},
useNewTabPage: true,
};
================================================
FILE: src/migrations/mocks/v1.ts
================================================
export default {
filtersStore: {
data: [
{
id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',
label: 'OctoLenses Issues',
predicates: [
{ type: 'type', value: 'issues' },
{ type: 'repo', value: 'rgehan/octolenses', negated: false },
{ type: 'status', value: 'open' },
],
},
{
id: '92da29c0-5216-11e9-ad7a-73a635a52ed2',
label: 'My Private Filter',
predicates: [
{ type: 'status', value: 'open' },
{
type: 'repo',
value: 'botify-hq/botify-report',
negated: false,
},
{ type: 'author', value: 'rgehan', negated: false },
{ type: 'status', value: 'open', negated: false },
],
},
],
},
settingsStore: {
language: null,
dateRange: 'last_week',
token: '<token>',
wasOnboarded: true,
darkMode: 'DISABLED',
schemaVersion: 1,
},
useNewTabPage: true,
};
================================================
FILE: src/migrations/mocks/v2-with-negated-predicates.ts
================================================
export default {
filtersStore: {
data: [
{
id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',
label: 'OctoLenses Issues',
provider: 'github',
predicates: [
{ type: 'type', value: 'issues' },
{ type: 'repo', value: 'rgehan/octolenses', negated: true },
{ type: 'status', value: 'open', negated: false },
{ type: 'author', value: 'rgehan', negated: false },
],
},
],
},
settingsStore: {
language: null,
dateRange: 'last_week',
wasOnboarded: true,
darkMode: 'DISABLED',
schemaVersion: 2,
},
useNewTabPage: true,
githubProvider: {
settings: {
token: '<token>',
},
},
};
================================================
FILE: src/migrations/mocks/v2.ts
================================================
export default {
filtersStore: {
data: [
{
id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',
label: 'OctoLenses Issues',
provider: 'github',
predicates: [
{ type: 'type', value: 'issues' },
{ type: 'repo', value: 'rgehan/octolenses', negated: false },
{ type: 'status', value: 'open' },
],
},
{
id: '92da29c0-5216-11e9-ad7a-73a635a52ed2',
label: 'My Private Filter',
provider: 'github',
predicates: [
{ type: 'status', value: 'open' },
{
type: 'repo',
value: 'botify-hq/botify-report',
negated: false,
},
{ type: 'author', value: 'rgehan', negated: false },
{ type: 'status', value: 'open', negated: false },
],
},
],
},
settingsStore: {
language: null,
dateRange: 'last_week',
wasOnboarded: true,
darkMode: 'DISABLED',
schemaVersion: 2,
},
useNewTabPage: true,
githubProvider: {
settings: {
token: '<token>',
},
},
};
================================================
FILE: src/migrations/mocks/v3-with-defaulted-operators.ts
================================================
export default {
filtersStore: {
data: [
{
id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',
label: 'OctoLenses Issues',
provider: 'github',
predicates: [
{ type: 'type', value: 'issues' },
{ type: 'repo', value: 'rgehan/octolenses', operator: 'not_equal' },
{ type: 'status', value: 'open' },
{ type: 'author', value: 'rgehan', operator: 'equal' },
],
},
],
},
settingsStore: {
language: null,
dateRange: 'last_week',
wasOnboarded: true,
darkMode: 'DISABLED',
schemaVersion: 3,
},
useNewTabPage: true,
githubProvider: {
settings: {
token: '<token>',
},
},
};
================================================
FILE: src/migrations/testing-utils.ts
================================================
/* tslint:disable no-console */
import { forEach, keys, pick, reduce } from 'lodash';
import { migrator } from './index';
import mocks from './mocks';
import { hydrateLocalStorageFromObject } from './utils';
declare global {
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
interface Window {
loadTestLocalStorage(name: string): void;
migrate(): void;
exportConfiguration(): void;
loadConfiguration(json: string): void;
}
}
window.loadTestLocalStorage = (name: string) => {
if (!mocks[name]) {
console.log('No such test data. Available keys: ' + keys(mocks).join(', '));
return;
}
console.log(`Loading test data: ${name}`);
hydrateLocalStorageFromObject(mocks[name]);
};
window.migrate = () => {
migrator.migrate();
};
/**
* Serializes the relevant data stores in an exportable format
*/
window.exportConfiguration = () => {
const data = pick(localStorage, [
'settingsStore',
'filtersStore',
'githubProvider',
'jiraProvider',
'useNewTabPage',
]);
const stringified = JSON.stringify(
reduce(
data,
(acc: any, value, key) => {
acc[key] = btoa(value);
return acc;
},
{}
)
);
console.log(
`In order to import that configuration, run:\n` +
`\twindow.loadConfiguration('${stringified}');`
);
};
/**
* Imports a previously serialized data store
*/
window.loadConfiguration = (json: string) => {
forEach(JSON.parse(json), (v, k) => {
localStorage.setItem(k, atob(v));
});
console.log('Import done.');
};
================================================
FILE: src/migrations/types.ts
================================================
export interface IMigration {
name: string;
shouldRun(): boolean;
run(): void;
}
================================================
FILE: src/migrations/utils.ts
================================================
export function getFromLocalStorage(key: string) {
const data = localStorage.getItem(key);
if (data) {
return JSON.parse(data);
}
return null;
}
export function saveToLocalStorage(key: string, value: any) {
const data = JSON.stringify(value);
localStorage.setItem(key, data);
}
export function hydrateLocalStorageFromObject(object: any) {
localStorage.clear();
Object.keys(object).forEach(key => {
saveToLocalStorage(key, object[key]);
});
}
export function dumpLocalStorageToObject() {
const object: Record<string, any> = {};
Object.keys(localStorage).forEach(key => {
object[key] = getFromLocalStorage(key);
});
return object;
}
================================================
FILE: src/migrations/v0-to-v1.test.ts
================================================
/* eslint-disable @typescript-eslint/camelcase */
import {
dumpLocalStorageToObject,
hydrateLocalStorageFromObject,
} from './utils';
import v0_to_v1 from './v0-to-v1';
import v0 from './mocks/v0';
import v1 from './mocks/v1';
describe('v0 to v1 migrations', () => {
beforeEach(() => {
localStorage.clear();
});
it('should run if no schemaVersion is set', () => {
hydrateLocalStorageFromObject(v0);
expect(new v0_to_v1().shouldRun()).toEqual(true);
});
it('should not run if a schemaVersion is set', () => {
hydrateLocalStorageFromObject(v1);
expect(new v0_to_v1().shouldRun()).toEqual(false);
});
it('should add schemaVersion to the settings', () => {
hydrateLocalStorageFromObject(v0);
new v0_to_v1().run();
const obj = dumpLocalStorageToObject();
expect(obj).toHaveProperty('settingsStore.schemaVersion', 1);
});
});
================================================
FILE: src/migrations/v0-to-v1.ts
================================================
/* tslint:disable no-console */
import { IMigration } from './types';
import { getFromLocalStorage, saveToLocalStorage } from './utils';
export default class implements IMigration {
public name = 'v0-to-v1';
public shouldRun(): boolean {
const settings = getFromLocalStorage('settingsStore');
if (settings && settings.schemaVersion === undefined) {
return true;
}
return false;
}
public run() {
const settings = getFromLocalStorage('settingsStore');
settings.schemaVersion = 1;
console.log('[migration] Upgrading schema version to 1');
saveToLocalStorage('settingsStore', settings);
}
}
================================================
FILE: src/migrations/v1-to-v2.test.ts
================================================
/* eslint-disable @typescript-eslint/camelcase */
import {
dumpLocalStorageToObject,
hydrateLocalStorageFromObject,
} from './utils';
import v1_to_v2 from './v1-to-v2';
import v1 from './mocks/v1';
import v1_withoutToken from './mocks/v1-without-token';
import v2 from './mocks/v2';
describe('v1 to v2 migrations', () => {
beforeEach(() => {
localStorage.clear();
});
it('should run if schemaVersion is 1', () => {
hydrateLocalStorageFromObject(v1);
expect(new v1_to_v2().shouldRun()).toEqual(true);
});
it('should not run if schemaVersion is not 1', () => {
hydrateLocalStorageFromObject(v2);
expect(new v1_to_v2().shouldRun()).toEqual(false);
});
it('should set all filters provider to "github" by default', () => {
hydrateLocalStorageFromObject(v1);
new v1_to_v2().run();
const obj = dumpLocalStorageToObject();
obj.filtersStore.data.map((filter: any) => {
expect(filter.provider).toEqual('github');
});
});
it('should not add a token if there is none yet', () => {
hydrateLocalStorageFromObject(v1_withoutToken);
expect(v1_withoutToken).not.toHaveProperty('settingsStore.token');
new v1_to_v2().run();
const obj = dumpLocalStorageToObject();
expect(obj).not.toHaveProperty('githubProvider.settings.token');
expect(obj).not.toHaveProperty('githubProvider');
});
it('should create a githubProvider.settings object containing the token', () => {
hydrateLocalStorageFromObject(v1);
expect(v1).toHaveProperty('settingsStore.token');
new v1_to_v2().run();
const obj = dumpLocalStorageToObject();
expect(obj).toHaveProperty('githubProvider.settings.token');
});
it('should set schemaVersion to 2 upon completion', () => {
hydrateLocalStorageFromObject(v1);
new v1_to_v2().run();
const obj = dumpLocalStorageToObject();
expect(obj).toHaveProperty('settingsStore.schemaVersion', 2);
});
});
================================================
FILE: src/migrations/v1-to-v2.ts
================================================
/* tslint:disable no-console */
import { ProviderType } from '../providers';
import { Filter } from '../store/filters';
import { IMigration } from './types';
import { getFromLocalStorage, saveToLocalStorage } from './utils';
export default class implements IMigration {
public name = 'v1-to-v2';
public shouldRun(): boolean {
const settings = getFromLocalStorage('settingsStore');
if (settings && settings.schemaVersion === 1) {
return true;
}
return false;
}
public run() {
this.defaultFiltersProviderToGithub();
this.moveTokenFromSettingsToGithubProvider();
this.upgradeSchemaVersion();
}
private defaultFiltersProviderToGithub() {
const filters = getFromLocalStorage('filtersStore');
console.log('[migration] Defaulting all filters providers to "github"');
filters.data.forEach((filter: Filter) => {
filter.provider = filter.provider || ProviderType.GITHUB;
});
saveToLocalStorage('filtersStore', filters);
}
private moveTokenFromSettingsToGithubProvider() {
const settings = getFromLocalStorage('settingsStore');
const githubProvider = getFromLocalStorage('githubProvider');
if (githubProvider || !settings.token) {
return;
}
console.log('[migration] Moving set token to github provider settings');
saveToLocalStorage('githubProvider', {
settings: { token: settings.token },
});
}
private upgradeSchemaVersion() {
const settings = getFromLocalStorage('settingsStore');
settings.schemaVersion = 2;
console.log('[migration] Upgrading schema version to 2');
saveToLocalStorage('settingsStore', settings);
}
}
================================================
FILE: src/migrations/v2-to-v3.test.ts
================================================
/* eslint-disable @typescript-eslint/camelcase */
import {
dumpLocalStorageToObject,
hydrateLocalStorageFromObject,
} from './utils';
import v2_to_v3 from './v2-to-v3';
import v2 from './mocks/v2-with-negated-predicates';
import v3 from './mocks/v3-with-defaulted-operators';
describe('v2 to v3 migrations', () => {
beforeEach(() => {
localStorage.clear();
});
it('should run if schemaVersion is 2', () => {
hydrateLocalStorageFromObject(v2);
expect(new v2_to_v3().shouldRun()).toEqual(true);
});
it('should properly default operators', () => {
hydrateLocalStorageFromObject(v2);
new v2_to_v3().run();
const obj = dumpLocalStorageToObject();
expect(obj).toEqual(v3);
});
it('should set schemaVersion to 3 upon completion', () => {
hydrateLocalStorageFromObject(v2);
new v2_to_v3().run();
const obj = dumpLocalStorageToObject();
expect(obj).toHaveProperty('settingsStore.schemaVersion', 3);
});
});
================================================
FILE: src/migrations/v2-to-v3.ts
================================================
/* tslint:disable no-console */
import { providers, ProviderType } from '../providers';
import { Filter } from '../store/filters';
import { IMigration } from './types';
import { getFromLocalStorage, saveToLocalStorage } from './utils';
export default class implements IMigration {
public name = 'v2-to-v3';
public shouldRun(): boolean {
const settings = getFromLocalStorage('settingsStore');
if (settings && settings.schemaVersion === 2) {
return true;
}
return false;
}
public run() {
this.defaultPredicatesOperators();
this.upgradeSchemaVersion();
}
private defaultPredicatesOperators() {
const filters = getFromLocalStorage('filtersStore');
// There shouldn't be any other provider in use for now
const provider = providers[ProviderType.GITHUB];
console.log('[migration] Defaulting all predicates operators');
filters.data.forEach((filter: Filter) => {
filter.predicates.forEach((predicate: any) => {
const isNegated = predicate.negated;
predicate.negated = undefined;
const predicateDefinition = provider.findPredicate(predicate.type);
// Shouldn't happen, but let's be safe
if (!predicateDefinition) {
return;
}
// Operator-less predicates don't have an `operator` key
if (predicateDefinition.operators.length === 0) {
return;
}
predicate.operator = isNegated ? 'not_equal' : 'equal';
});
});
saveToLocalStorage('filtersStore', filters);
}
private upgradeSchemaVersion() {
const settings = getFromLocalStorage('settingsStore');
settings.schemaVersion = 3;
console.log('[migration] Upgrading schema version to 3');
saveToLocalStorage('settingsStore', settings);
}
}
================================================
FILE: src/pages/Dashboard/Dashboard.tsx
================================================
import cx from 'classnames';
import ExtendableError from 'es6-error';
import { get, size } from 'lodash';
import { computed } from 'mobx';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { Loader } from '../../components';
import { FilterEditModal } from '../../containers';
import { providers } from '../../providers';
import { FiltersStore } from '../../store/filters';
import { SettingsStore } from '../../store/settings';
import { FilterLinkContainer } from './FilterLinkContainer';
interface IProps {
filtersStore?: FiltersStore;
settingsStore?: SettingsStore;
}
@inject('filtersStore', 'settingsStore')
@observer
export class Dashboard extends React.Component<IProps> {
public state = {
filterModal: { isOpen: false, mode: 'adding' },
metaPressed: false,
};
componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown);
window.addEventListener('keyup', this.handleKeyUp);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown);
window.removeEventListener('keyup', this.handleKeyUp);
}
private handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Meta') {
this.setState({ metaPressed: true });
}
};
private handleKeyUp = (event: KeyboardEvent) => {
if (event.key === 'Meta') {
this.setState({ metaPressed: false });
}
};
@computed
get selectedFilter() {
const { filtersStore, settingsStore } = this.props;
const filter = filtersStore.findFilter(settingsStore.selectedFilterId);
const firstFilter = filtersStore.getFirstFilter();
return filter || firstFilter;
}
public handleFilterSelected = (filterId: string) => {
const { filtersStore, settingsStore } = this.props;
if (filterId === settingsStore.selectedFilterId) {
return;
}
// Clear the notifications of the filter that was selected
filtersStore
.findFilter(settingsStore.selectedFilterId)
.clearNewItemsNotifications();
// Select the new filter
settingsStore.selectedFilterId = filterId;
};
public handleCloneFilter = () => {
const { filtersStore } = this.props;
const { id } = filtersStore.cloneFilter(this.selectedFilter.id);
this.handleFilterSelected(id);
};
public handleRefreshFilter = () => {
this.selectedFilter.invalidateCache();
};
public handleRefreshAllFilters = () => {
this.props.filtersStore.fetchAllFilters();
};
public handleDeleteFilter = () => {
const { filtersStore, settingsStore } = this.props;
if (!this.selectedFilter || filtersStore.count === 1) {
return;
}
// Find out the index (in the list) of the filter
const currentFilterIndex = filtersStore.findFilterIndex(
this.selectedFilter.id
);
// Find out which filter we'll have to select once removed
const isDeletingLastFilter = currentFilterIndex === filtersStore.count - 1;
const newlySelectedFilterIndex = isDeletingLastFilter
? currentFilterIndex - 1
: currentFilterIndex + 1;
// Find the actual UUID of the filter
const realIndex = filtersStore.getFilterAt(newlySelectedFilterIndex).id;
// Remove the filter, then select the next one
filtersStore.removeFilter(this.selectedFilter.id);
settingsStore.selectedFilterId = realIndex;
};
/*
* Modal logic
*/
public handleOpenFilterModal = (mode: string) => () => {
this.setState({
filterModal: {
isOpen: true,
mode,
},
});
};
public handleCloseFilterModal = () => {
this.setState({
filterModal: {
isOpen: false,
},
});
};
public reorderFilters = ({ oldIndex, newIndex }: any) => {
const { filtersStore } = this.props;
// Do nothing if the user cancelled the drag
if (oldIndex === newIndex) {
return;
}
// Select the filter we want to move...
this.handleFilterSelected(filtersStore.getFilterAt(oldIndex).id);
// ...and move it
filtersStore.swapFilters(oldIndex, newIndex);
};
public render() {
const { filtersStore, settingsStore } = this.props;
const { filterModal, metaPressed } = this.state;
const LINKS = [
{
handler: this.handleOpenFilterModal('adding'),
text: 'Add',
icon: 'far fa-plus-square',
},
{
handler: this.handleOpenFilterModal('editing'),
text: 'Edit',
icon: 'far fa-edit',
},
{
handler: this.handleCloneFilter,
text: 'Clone',
icon: 'far fa-clone',
},
metaPressed
? {
handler: this.handleRefreshAllFilters,
text: 'Refresh All',
icon: 'fas fa-sync-alt',
}
: {
handler: this.handleRefreshFilter,
text: 'Refresh',
icon: 'fas fa-sync-alt',
},
{
handler: this.handleDeleteFilter,
text: 'Delete',
icon: 'far fa-trash-alt',
},
];
return (
<div className="flex items-start w-full h-full pt-16">
<div className="flex flex-col w-48 sticky top-4">
<FilterLinkContainer
links={filtersStore.getFilters()}
selectedFilterId={get(this.selectedFilter, 'id')}
onFilterSelected={this.handleFilterSelected}
onSortEnd={this.reorderFilters}
lockAxis="y"
lockToContainerEdges
useDragHandle
/>
<div className="flex flex-col items-end pr-5 mt-10">
{LINKS.map(({ handler, text, icon }) => (
<div
onClick={handler}
key={text}
className={cx(
'mb-3 cursor-pointer select-none text-gray-600',
settingsStore.isDark
? 'hover:text-gray-500'
: 'hover:text-gray-900'
)}
>
{text} <i className={cx(icon, 'ml-1 w-6 opacity-75')} />
</div>
))}
</div>
</div>
<div
className={cx(
'flex-1 flex flex-col shadow-xl rounded-lg mb-16 min-w-0',
settingsStore.isDark ? 'bg-gray-800 text-white' : 'bg-white'
)}
data-id="filter-results"
>
{this.renderResults()}
</div>
{filterModal.isOpen && (
<FilterEditModal
initialFilter={
filterModal.mode === 'editing' ? this.selectedFilter : null
}
onClose={this.handleCloseFilterModal}
/>
)}
</div>
);
}
public renderResults() {
if (!this.selectedFilter) {
return null;
}
if (this.selectedFilter.loading) {
return <Loader size={50} className="my-10" />;
}
if (this.selectedFilter.error) {
const errorMessage =
this.selectedFilter.error instanceof ExtendableError
? this.selectedFilter.error.message
: 'Something failed, sorry.';
return (
<div className="flex-1 flex items-center justify-center select-none text-2xl my-10 mx-0 text-gray-500">
<i className="fas fa-exclamation-triangle mr-2" />
{errorMessage}
</div>
);
}
if (size(this.selectedFilter.data) === 0) {
return (
<div className="flex-1 flex items-center justify-center select-none text-2xl my-10 mx-0 text-gray-500">
<i className="fa fa-search mr-2" />
No results.
</div>
);
}
const CardComponent = providers[this.selectedFilter.provider].cardComponent;
return this.selectedFilter.data.map((itemData, index) => (
<CardComponent
key={index}
data={itemData}
isNew={this.selectedFilter.isItemNew(itemData)}
/>
));
}
}
================================================
FILE: src/pages/Dashboard/FilterLinkContainer.tsx
================================================
import React from 'react';
import { SortableContainer } from 'react-sortable-hoc';
import { FilterLink } from '../../components';
import { Filter } from '../../store/filters';
interface IProps {
links: Filter[];
selectedFilterId: string;
onFilterSelected: (id: string) => void;
}
export const FilterLinkContainer = SortableContainer<IProps>(
({ links, selectedFilterId, onFilterSelected }: IProps) => (
<div data-id="filter-links">
{links.map((link: Filter, index: number) => (
<FilterLink
key={link.id}
index={index}
filter={link}
isSelected={link.id === selectedFilterId}
onClick={() => onFilterSelected(link.id)}
/>
))}
</div>
)
);
================================================
FILE: src/pages/Dashboard/index.ts
================================================
export { Dashboard } from './Dashboard';
================================================
FILE: src/pages/Discover/Discover.scss
================================================
.Discover {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
&__Actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-bottom: 20px;
> div {
margin-left: 20px;
}
}
&__ReposList {
flex: 1;
display: flex;
@media screen and (max-width: 768px) {
flex-direction: column;
}
@media screen and (min-width: 769px) {
flex-wrap: wrap;
}
}
}
================================================
FILE: src/pages/Discover/Discover.tsx
================================================
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import { Dropdown, Loader } from '../../components';
import { DATES, DateType } from '../../constants/dates';
import { LANGUAGES } from '../../constants/languages';
import { RepoCard } from '../../containers';
import { SettingsStore } from '../../store/settings';
import { TrendsStore } from '../../store/trends';
import './Discover.scss';
interface IInnerProps {
settingsStore?: SettingsStore;
trendsStore?: TrendsStore;
}
export const Discover = compose<IInnerProps, {}>(
inject('settingsStore', 'trendsStore'),
observer
)(({ settingsStore, trendsStore }) => {
function handleChangeLanguage({ value }: { value: string }) {
settingsStore.updateLanguage(value);
trendsStore.fetchTrendingRepos();
}
function handleChangeDateRange({ value }: { value: string }) {
settingsStore.updateDateRange(value as DateType);
trendsStore.fetchTrendingRepos();
}
return (
<div className="Discover h-full w-full flex flex-col">
<div className="flex items-end justify-end pb-4 h-16">
<Dropdown
name="language"
items={LANGUAGES}
value={settingsStore.language}
onChange={handleChangeLanguage}
className="mr-4"
/>
<Dropdown
name="dateRange"
items={DATES}
value={settingsStore.dateRange}
onChange={handleChangeDateRange}
/>
</div>
<div className="Discover__ReposList -ml-6">
{trendsStore.loading ? (
<Loader />
) : (
trendsStore.data.map(repo => <RepoCard key={repo.id} repo={repo} />)
)}
</div>
</div>
);
});
================================================
FILE: src/pages/Discover/index.ts
================================================
export { Discover } from './Discover';
================================================
FILE: src/pages/index.ts
================================================
export { Discover } from './Discover';
export { Dashboard } from './Dashboard';
================================================
FILE: src/providers/AbstractProvider.ts
================================================
import { observable } from 'mobx';
import { persist } from 'mobx-persist';
import { Filter } from '../store/filters';
import { SettingsStore } from '../store/settings';
import { Predicate } from './types';
interface ISettingsComponentProps {
settings: SettingsStore;
}
type SettingsComponent = ({ settings }: ISettingsComponentProps) => JSX.Element;
type CardComponent = React.ComponentType<{ data: any }>;
export abstract class AbstractProvider<T = {}> {
/**
* Unique identifier of the provider
*/
public id: string;
/**
* Name of the provider, as displayed in the Settings sidebar
*/
public label: string;
/**
* Component used to render the provider settings panel
*/
public settingsComponent: SettingsComponent;
/**
* Component used to render the provider's data on the dashboard
*/
public cardComponent: CardComponent;
/**
* A place for the provider to store its specific settings
*/
@persist('object')
@observable
public settings: T = {} as T;
/**
* Called after the app has been booted, so that we can perform initial
* data fetching and initialization tasks.
*/
public abstract initialize(): Promise<void>;
/**
* Returns an array of available predicates for the provider
*/
public abstract getAvailablePredicates(): Predicate[];
/**
* Retrieve the definition of a predicate from its name
* @param name Name of the predicate we want to retrieve
*/
public abstract findPredicate(name: string): Predicate;
/**
* Fetch a filter associated to this provider
* @param filter
* @param providerSettings
*/
public abstract fetchFilter(filter: Filter): Promise<any[]>;
/**
* Resolve the unique identifier associated to a filter item
* @param item A filter item
*/
public abstract resolveFilterItemIdentifier(item: any): string;
}
================================================
FILE: src/providers/github/components/IssueCard/CheckStatusIndicator.tsx
================================================
import cx from 'classnames';
import React from 'react';
import { IIssue } from './IssueCard';
import { IssueStatus } from './types';
interface IProps {
status: IIssue['status'];
}
const STATUS_TO_ICON = {
[IssueStatus.SUCCESS]: 'fas fa-check text-green-500',
[IssueStatus.FAILURE]: 'fas fa-times text-red-500',
[IssueStatus.PENDING]: 'fas fa-circle text-orange-500',
};
const STATUS_TO_LABEL = {
[IssueStatus.SUCCESS]: 'All checks passed',
[IssueStatus.FAILURE]: 'Some checks have failed',
[IssueStatus.PENDING]: 'Checks are running',
};
export const CheckStatusIndicator = ({ status }: IProps) => {
if (status === IssueStatus.UNKNOWN) {
return null;
}
const icon = STATUS_TO_ICON[status];
const label = STATUS_TO_LABEL[status];
return <i className={cx('text-sm ml-2', icon)} title={label} />;
};
================================================
FILE: src/providers/github/components/IssueCard/ConflictIndicator.tsx
================================================
import React from 'react';
interface IProps {
conflicting: boolean;
}
export const ConflictIndicator = ({ conflicting }: IProps) => {
if (!conflicting) {
return null;
}
return (
<i
className="fas fa-exclamation-triangle text-red-500 text-sm ml-2"
title="There are conflicts on this PR"
/>
);
};
================================================
FILE: src/providers/github/components/IssueCard/ContextualDropdown.tsx
================================================
import cx from 'classnames';
import ClipboardJS from 'clipboard';
import { inject, observer } from 'mobx-react';
import React, { useEffect } from 'react';
import { compose } from 'recompose';
import styled from 'styled-components';
import { toast } from '../../../../components/ToastManager/ToastManager';
import { SettingsStore } from '../../../../store/settings';
import { IIssue } from './IssueCard';
const Wrapper = styled.div`
.overlay {
display: none;
}
:hover {
.overlay {
display: flex;
}
}
`;
const Overlay = styled.div<{ dark: boolean }>`
top: 16px;
left: 2px;
:before {
content: '';
height: 0px;
width: 1px;
border-bottom: 4px solid ${({ dark }) => (dark ? '#606f7b' : 'white')};
border-left: 4px solid transparent;
border-right: 4px solid transparent;
position: absolute;
top: -4px;
left: 6px;
}
`;
const makeActions = (issue: IIssue) => {
const type = issue.pull_request ? 'Pull request' : 'Issue';
return [
{
label: 'Copy Link',
'data-clipboard-text': issue.html_url,
onClick: () => toast(`${type} link was copied to the clipboard`, 'info'),
},
{
label: 'Copy Title',
'data-clipboard-text': issue.title,
onClick: () => toast(`${type} title was copied to the clipboard`, 'info'),
},
];
};
interface IProps {
issue: IIssue;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const ContextualDropdown = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ issue, settingsStore }) => {
useEffect(() => {
const clipboard = new ClipboardJS('[data-clipboard-text]');
return () => clipboard.destroy();
});
const actions = makeActions(issue);
return (
<Wrapper className="inline-block relative">
<i className="fa fa-caret-down py-1 px-2 -mt-1 cursor-pointer" />
<Overlay
dark={settingsStore.isDark}
className={cx([
'overlay',
'absolute py-1',
settingsStore.isDark
? 'bg-gray-700'
: 'bg-white border border-gray-200',
'whitespace-nowrap rounded shadow-lg',
'flex flex-col',
])}
>
{actions.map(({ label, ...otherProps }) => (
<div
className="text-sm px-4 py-1 cursor-pointer hover:underline"
key={label}
{...otherProps}
>
{label}
</div>
))}
</Overlay>
</Wrapper>
);
});
================================================
FILE: src/providers/github/components/IssueCard/IssueCard.tsx
================================================
import cx from 'classnames';
import { get } from 'lodash';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import * as timeago from 'timeago.js';
import { SettingsStore } from '../../../../store/settings';
import { CheckStatusIndicator } from './CheckStatusIndicator';
import { ConflictIndicator } from './ConflictIndicator';
import { ContextualDropdown } from './ContextualDropdown';
import { IssueStatusIndicator } from './IssueStatusIndicator';
import { LabelBadge } from './LabelBadge';
import { IssueStatus, TimelineItem, TimelineItemType } from './types';
export interface IIssue {
type: 'PullRequest' | 'Issue';
title: string;
url: string;
html_url: string;
state: 'open' | 'closed' | 'merged';
isDraft: boolean;
status: IssueStatus;
number: number;
createdAt: string;
conflicting: boolean;
pull_request?: {
url: string;
};
author: {
url: string;
login: string;
avatarUrl: string;
};
repository: {
url: string;
nameWithOwner: string;
};
reviews?: {
totalCount: number;
};
comments?: {
totalCount: number;
};
labels: Array<{ color: string; name: string }>;
timelineItems?: TimelineItem[];
}
interface IProps {
data: IIssue;
isNew: boolean;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const IssueCard = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ data: issue, isNew, settingsStore }) => {
const linkStyle = settingsStore.isDark
? 'text-blue-400'
: 'text-blue-500 hover:text-blue-600';
const firstTimelineItem = get(issue, 'timelineItems.nodes.0');
function getTotalCommentsCount() {
return (
get(issue, 'reviews.totalCount', 0) + get(issue, 'comments.totalCount', 0)
);
}
function getLastActivityDate(item: TimelineItem) {
switch (item.__typename) {
case TimelineItemType.ISSUE_COMMENT:
return item.createdAt;
case TimelineItemType.PULL_REQUEST_COMMIT:
return item.commit.committedDate;
case TimelineItemType.PULL_REQUEST_REVIEW:
return item.createdAt;
}
}
return (
<div
className={cx(
'p-6 flex border-l-4',
isNew ? 'border-blue-500' : 'border-transparent'
)}
>
<div className="flex items-center justify-center flex-shrink-0 pr-4">
<div
className={cx(
'w-16 h-16 rounded-full overflow-hidden',
settingsStore.isDark ? 'bg-gray-700' : 'bg-gray-400'
)}
>
<img src={issue.author.avatarUrl} />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex-1 flex justify-between items-center">
<div className="flex-1 flex items-center mb-1 min-w-0">
<IssueStatusIndicator issue={issue} />
<span className={cx('truncate pb-1 min-w-0', linkStyle)}>
<a
className={cx('hover:underline', linkStyle)}
href={issue.repository.url}
>
{issue.repository.nameWithOwner}
</a>
<span className="mx-1">•</span>
<a
className={cx('text-lg hover:underline', linkStyle)}
href={issue.url}
title={issue.title}
>
{issue.title}{' '}
</a>
</span>
<CheckStatusIndicator status={issue.status} />
<ConflictIndicator conflicting={issue.conflicting} />
</div>
<a
href={issue.url}
className="ml-2 text-gray-600 hover:text-gray-500"
>
<i className="far fa-comment-alt" /> {getTotalCommentsCount()}
</a>
</div>
<div
className={cx(
'text-xs',
settingsStore.isDark ? 'text-gray-500' : 'text-gray-700'
)}
>
#{issue.number} opened {timeago.format(issue.createdAt)} by{' '}
<a
href={issue.author.url}
className={cx(
'no-underline hover:underline',
settingsStore.isDark ? 'text-gray-500' : 'text-gray-700'
)}
>
{issue.author.login}
</a>
{firstTimelineItem && (
<React.Fragment>
{', '}
<span>
last activity{' '}
{timeago.format(getLastActivityDate(firstTimelineItem))}
</span>
</React.Fragment>
)}
<ContextualDropdown issue={issue} />
</div>
<div className="flex flex-wrap gap-y-1 gap-x-1 mt-3">
{issue.labels.map(label => (
<LabelBadge key={label.name} label={label} />
))}
</div>
</div>
</div>
);
});
================================================
FILE: src/providers/github/components/IssueCard/IssueStatusIndicator.tsx
================================================
import cx from 'classnames';
import React from 'react';
import { IIssue } from './IssueCard';
import {assertUnreachable} from "../../../../lib/assertUnreachable";
interface IProps {
issue: Pick<IIssue, 'state' | 'isDraft' | 'type'>;
}
export const IssueStatusIndicator = ({ issue }: IProps) => (
<i
className={cx(
'mr-2',
issue.type === 'PullRequest'
? 'fas fa-code-branch'
: 'fas fa-exclamation-circle',
getColor(issue.state.toLowerCase() as IIssue['state'], issue.isDraft)
)}
/>
);
function getColor(state: IIssue['state'], isDraft: boolean)
{
switch (state) {
case 'open':
return isDraft ? 'text-gray-500' : 'text-green-500';
case 'closed':
return 'text-red-500';
case 'merged':
return 'text-purple-500';
default:
return assertUnreachable(state, 'text-gray-500');
}
}
================================================
FILE: src/providers/github/components/IssueCard/LabelBadge.tsx
================================================
import cx from 'classnames';
import contrast from 'contrast';
import React from 'react';
interface IProps {
label: {
name: string;
color: string;
};
}
export const LabelBadge = ({ label }: IProps) => {
const background = `#${label.color}`;
const isDark = contrast(background) === 'dark';
return (
<div
className={cx(
'text-xs whitespace-nowrap py-1 px-2 rounded',
isDark ? 'text-white' : 'text-gray-900'
)}
style={{ background }}
>
{label.name}
</div>
);
};
================================================
FILE: src/providers/github/components/IssueCard/index.ts
================================================
export { IssueCard } from './IssueCard';
================================================
FILE: src/providers/github/components/IssueCard/types.ts
================================================
export enum IssueStatus {
PENDING = 'PENDING',
SUCCESS = 'SUCCESS',
FAILURE = 'FAILURE',
UNKNOWN = 'UNKNOWN',
}
export enum TimelineItemType {
ISSUE_COMMENT = 'IssueComment',
PULL_REQUEST_COMMIT = 'PullRequestCommit',
PULL_REQUEST_REVIEW = 'PullRequestReview',
}
export type TimelineItem =
| IIssueComment
| IPullRequestCommit
| IPullRequestReview;
interface IIssueComment {
__typename: TimelineItemType.ISSUE_COMMENT;
createdAt: string;
}
interface IPullRequestCommit {
__typename: TimelineItemType.PULL_REQUEST_COMMIT;
commit: {
committedDate: string;
};
}
interface IPullRequestReview {
__typename: TimelineItemType.PULL_REQUEST_REVIEW;
createdAt: string;
}
================================================
FILE: src/providers/github/components/ProfileCard.tsx
================================================
import cx from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import { IGithubProfile } from '../index';
import { SettingsStore } from '../../../store/settings';
interface IProps {
profile: IGithubProfile;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const ProfileCard = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ profile, settingsStore }) => {
if (!profile) {
return null;
}
return (
<div className="flex mb-8">
<div
className={cx(
'w-16 h-16 rounded-full overflow-hidden mr-3',
settingsStore.isDark ? 'bg-gray-700' : 'bg-gray-400'
)}
>
<img src={profile.avatar_url} />
</div>
<div className="pt-2">
<div
className={settingsStore.isDark ? 'text-gray-300' : 'text-gray-800'}
>
{profile.name}
</div>
<a
href={profile.html_url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-600 mt-1"
>
@{profile.login}
</a>
</div>
</div>
);
});
================================================
FILE: src/providers/github/components/Settings.tsx
================================================
import cx from 'classnames';
import { inject, observer } from 'mobx-react';
import React, { useState } from 'react';
import { compose } from 'recompose';
import styled from 'styled-components';
import { GithubProvider } from '..';
import { Button, ButtonType } from '../../../components/Button';
import { toast } from '../../../components/ToastManager';
import { SettingsStore } from '../../../store/settings';
import { ProfileCard } from './ProfileCard';
const CREATE_TOKEN_URL =
'https://github.com/settings/tokens/new?scopes=repo&description=octolenses';
const Input = styled.input<{ dark?: boolean }>`
::placeholder {
color: ${({ dark }) => (dark ? '#8795a1' : '#b8c2cc')};
opacity: 1;
}
`;
interface IProps {
provider: GithubProvider;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const Settings = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ provider, settingsStore }) => {
const [token, setToken] = useState(provider.settings.token || '');
function handleSubmit() {
provider.setToken(token);
toast('Token was saved', 'info');
}
return (
<div className="flex-1 flex flex-col items-stretch">
<ProfileCard profile={provider.profile} />
<div className="font-medium">Github Personal Access Token</div>
<div className="mt-4 leading-normal">
<p>
You can generate a Personal Access Token on{' '}
<a
className="text-blue-500"
href={CREATE_TOKEN_URL}
target="_blank"
rel="noopener noreferrer"
>
this page
</a>
.<br />
It needs to have the following scope:{' '}
<span
className={cx(
'font-mono px-2 rounded',
settingsStore.isDark ? 'bg-gray-800' : 'bg-gray-100'
)}
>
repo
</span>
</p>
</div>
<div className="relative flex items-center mt-4">
<Input
id="token"
type="password"
value={token}
onChange={(event: any) => setToken(event.target.value)}
placeholder="xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx"
dark={settingsStore.isDark}
className={cx(
'w-full rounded outline-none pl-10 pr-3 py-2 text-gray-600 tracking-wider font-mono',
settingsStore.isDark ? 'bg-gray-800' : 'bg-gray-100'
)}
/>
<i
className={cx(
'fas fa-key absolute left-0 ml-3',
settingsStore.isDark ? 'text-gray-600' : 'text-gray-500'
)}
/>
</div>
<div className="mt-8 flex justify-end">
<Button onClick={handleSubmit} type={ButtonType.PRIMARY}>
Save
</Button>
</div>
</div>
);
});
================================================
FILE: src/providers/github/fetchers/client.ts
================================================
import { get, pickBy } from 'lodash';
import {
InvalidCredentials,
NeedTokenError,
RateLimitError,
} from '../../../errors';
interface IClientParams {
endpoint: string;
token?: string;
qs?: string;
body?: string;
method?: 'GET' | 'POST';
}
/**
* Fetch data from GitHub API
*/
export const client = async ({
endpoint,
token,
qs,
body,
method = 'GET',
}: IClientParams) => {
const url = `https://api.github.com${endpoint}?${qs || ''}`;
const response = await fetch(url, {
body: method === 'POST' ? body : undefined,
method,
headers: pickBy({
'User-Agent': 'OctoLenses Github Dashboard',
Authorization: token && `Bearer ${token}`,
Accept: 'application/vnd.github.antiope-preview+json', // Allow access to Previews API
}),
});
if (!response.ok) {
await handleErrorResponse(response);
}
return await response.json();
};
/**
* Handle an error response from GitHub API
*/
const handleErrorResponse = async (response: Response) => {
const status = response.status;
const { message = '', errors = [] } = await response.json();
const firstErrorMessage = get(errors, '0.message', '');
if (status === 401 && message.includes('Bad credentials')) {
throw new InvalidCredentials();
}
if (status === 403 && message.includes('API rate limit')) {
const rateLimitReset = response.headers.get('X-RateLimit-Reset');
const remainingRateLimit = Number(rateLimitReset) - Date.now() / 1000;
throw new RateLimitError(remainingRateLimit);
}
if (
status === 422 &&
firstErrorMessage.includes('do not exist or you do not have permission')
) {
throw new NeedTokenError();
}
};
================================================
FILE: src/providers/github/fetchers/graphql/query.ts
================================================
const commonFields = `
url
title
state
number
createdAt
author {
url
login
avatarUrl
}
labels(first: 10) {
edges {
node {
name
color
}
}
}
repository {
nameWithOwner
url
}
comments {
totalCount
}
`;
const issueFragment = `
fragment IssueFragment on Issue {
${commonFields}
}`;
const pullRequestFragment = `
fragment PullRequestFragment on PullRequest {
${commonFields}
mergeable
isDraft
reviews {
totalCount
}
commits(last: 1) {
nodes {
commit {
status {
state
}
checkSuites(first: 1) {
nodes {
status
conclusion
app {
name
}
}
}
}
}
}
timelineItems(itemTypes: [PULL_REQUEST_COMMIT, PULL_REQUEST_REVIEW, ISSUE_COMMENT], last: 1) {
nodes {
__typename
... on IssueComment {
createdAt
}
... on PullRequestReview {
createdAt
}
... on PullRequestCommit {
commit {
committedDate
}
}
}
}
}`;
export const makeQuery = (filterString: string) => `
${pullRequestFragment}
${issueFragment}
query results {
search(query: "${filterString}", type: ISSUE, first: 100) {
edges {
node {
__typename
... on PullRequest {
...PullRequestFragment
}
... on Issue {
...IssueFragment
}
}
}
}
}`;
================================================
FILE: src/providers/github/fetchers/graphql/search.ts
================================================
import { chain, omit, map } from 'lodash';
import { Cache } from '../../../../lib/cache';
import { Filter } from '../../../../store/filters';
import { client } from '../client';
import { makeQuery } from './query';
import {
extractConflictStatus,
extractGraphqlLabels,
extractGraphqlStatus,
} from './utils';
/**
* Fetch a filter using the shiny GraphQL API
* @param {object} options.filter
*/
export const search = async (filter: Filter, token: string) => {
const filterString = chain(filter.predicates)
.map(predicate => filter.serializePredicate(predicate))
.join(' ')
.replace(/"/g, '\\"')
.value();
const cacheKey = `github.graphql.${filter.hash}`;
const response = await Cache.remember(cacheKey, 60, async () =>
client({
endpoint: '/graphql',
method: 'POST',
body: JSON.stringify({ query: makeQuery(filterString) }),
token,
})
);
return formatResponse(response);
};
/**
* Format a graphql response so that it's easy to use
*/
export const formatResponse = (response: any) => {
const issues = map(response.data.search.edges, 'node');
return issues.map(issue => ({
...omit(issue, ['commits', '__typename', 'mergeable']),
type: issue.__typename,
status: extractGraphqlStatus(issue),
labels: extractGraphqlLabels(issue),
conflicting: extractConflictStatus(issue),
}));
};
================================================
FILE: src/providers/github/fetchers/graphql/utils.ts
================================================
import { get, map } from 'lodash';
import { IssueStatus } from '../../components/IssueCard/types';
export const COMMIT_STATUS_TO_STATUS: Record<any, IssueStatus> = {
EXPECTED: IssueStatus.UNKNOWN,
ERROR: IssueStatus.FAILURE,
FAILURE: IssueStatus.FAILURE,
PENDING: IssueStatus.PENDING,
SUCCESS: IssueStatus.SUCCESS,
};
export const CHECK_CONCLUSION_TO_STATUS: Record<any, IssueStatus> = {
ACTION_REQUIRED: IssueStatus.UNKNOWN,
TIMED_OUT: IssueStatus.UNKNOWN,
CANCELLED: IssueStatus.UNKNOWN,
FAILURE: IssueStatus.FAILURE,
SUCCESS: IssueStatus.SUCCESS,
NEUTRAL: IssueStatus.UNKNOWN,
};
export function extractGraphqlStatus(issue: any) {
const commitStatus = get(issue, 'commits.nodes.0.commit.status.state');
const checkStatus = get(issue, 'commits.nodes.0.commit.checkSuites.nodes.0');
// Old status API
if (commitStatus) {
return COMMIT_STATUS_TO_STATUS[commitStatus];
}
// New check status API
if (checkStatus) {
const { status, conclusion } = checkStatus;
if (status !== 'COMPLETED') {
return IssueStatus.PENDING;
}
return CHECK_CONCLUSION_TO_STATUS[conclusion];
}
return IssueStatus.UNKNOWN;
}
export function extractGraphqlLabels(issue: any) {
return map(issue.labels.edges, 'node');
}
export function extractConflictStatus(issue: any) {
return issue.mergeable === 'CONFLICTING';
}
================================================
FILE: src/providers/github/fetchers/index.ts
================================================
import { IGithubSettings } from '..';
import { Filter } from '../../../store/filters';
import { search as graphqlSearch } from './graphql/search';
import { search as restSearch } from './rest/search';
export const fetchFilter = async (
filter: Filter,
settings: IGithubSettings
) => {
if (settings.token) {
return graphqlSearch(filter, settings.token);
}
return restSearch(filter);
};
================================================
FILE: src/providers/github/fetchers/rest/profile.ts
================================================
import { client } from '../client';
export const fetchProfile = async (token: string) => {
return client({ endpoint: '/user', token });
};
================================================
FILE: src/providers/github/fetchers/rest/search.ts
================================================
import { chain, map, pick } from 'lodash';
import { Cache } from '../../../../lib/cache';
import { Filter } from '../../../../store/filters';
import { IssueStatus } from '../../components/IssueCard/types';
import { client } from '../client';
/**
* Fetch a filter on the old REST API. This is only supposed to be
* used when the user has not set a token. This won't return all the
* value we need (for example there are no build status).
* @param {object} options.filter
*/
export const search = async (filter: Filter) => {
const filterString = chain(filter.predicates)
.map(predicate => filter.serializePredicate(predicate))
.map(encodeURIComponent)
.join('+')
.value();
const cacheKey = `github.rest.${filter.hash}`;
const { items: issues = [] } = await Cache.remember(cacheKey, 60, async () =>
client({
endpoint: '/search/issues',
qs: `per_page=100&q=${filterString}`,
})
);
return formatResponse(issues);
};
/**
* Format a REST response so that it's compatible with the GraphQL one
*/
export const formatResponse = (response: any) => {
return map(response, issue => ({
url: issue.html_url,
title: issue.title,
number: issue.number,
state: issue.state,
createdAt: issue.created_at,
author: {
url: issue.user.html_url,
login: issue.user.login,
avatarUrl: issue.user.avatar_url,
},
repository: {
nameWithOwner: parseRestRepoName(issue.repository_url),
url: parseRestRepoUrl(issue.repository_url),
},
labels: extractRestLabels(issue),
type: issue.pull_request ? 'PullRequest' : 'Issue',
comments: { totalCount: issue.comments },
reviews: { totalCount: 0 },
status: IssueStatus.UNKNOWN,
}));
};
const extractRestLabels = (issue: any) =>
map(issue.labels, label => pick(label, ['color', 'name']));
const parseRestRepoName = (url: string) =>
chain(url)
.split('/')
.slice(-2)
.join('/')
.value();
const parseRestRepoUrl = (apiUrl: string) =>
apiUrl.replace('api.github.com/repos/', 'github.com/');
================================================
FILE: src/providers/github/index.tsx
================================================
import { find } from 'lodash';
import { action, observable } from 'mobx';
import React from 'react';
import { Filter } from '../../store/filters';
import { AbstractProvider } from '../AbstractProvider';
import { IssueCard } from './components/IssueCard';
import { Settings } from './components/Settings';
import { fetchFilter } from './fetchers';
import { fetchProfile } from './fetchers/rest/profile';
import { availablePredicates } from './predicates';
export interface IGithubSettings {
token: string;
}
export interface IGithubProfile {
login: string;
name: string;
avatar_url: string;
html_url: string;
}
export class GithubProvider extends AbstractProvider<IGithubSettings> {
public id = 'github';
public label = 'GitHub';
public settingsComponent = () => <Settings provider={this} />;
public cardComponent = IssueCard;
@observable
public profile: IGithubProfile = null;
@action.bound
public async initialize() {
await this.fetchProfile();
}
public async fetchFilter(filter: Filter) {
return fetchFilter(filter, this.settings);
}
public resolveFilterItemIdentifier(item: any) {
return item.number;
}
public getAvailablePredicates = () => availablePredicates;
public findPredicate(name: string) {
return find(this.getAvailablePredicates(), { name });
}
@action.bound
public async fetchProfile() {
if (!this.settings.token) {
return;
}
this.profile = await fetchProfile(this.settings.token);
}
@action.bound
public setToken(token: string) {
this.settings.token = token;
if (token) {
this.fetchProfile();
} else {
this.profile = null;
}
}
}
export const github = new GithubProvider();
================================================
FILE: src/providers/github/predicates/createdOrUpdatedAt.ts
================================================
import moment from 'moment';
import { IDropdownPredicate, PredicateType } from '../../types';
enum Preset {
ONE_HOUR_AGO = 'one_hour_ago',
ONE_DAY_AGO = 'one_day_ago',
ONE_WEEK_AGO = 'one_week_ago',
ONE_MONTH_AGO = 'one_month_ago',
}
enum Operator {
MORE_THAN = 'more_than',
LESS_THAN = 'less_than',
}
function makeDatePredicate(name: string, label: string, githubField: string): IDropdownPredicate {
return {
name,
label,
type: PredicateType.DROPDOWN,
operators: [
{ value: Operator.MORE_THAN, label: 'More than' },
{ value: Operator.LESS_THAN, label: 'Less than' },
],
choices: [
{ value: Preset.ONE_HOUR_AGO, label: '1 hour ago' },
{ value: Preset.ONE_DAY_AGO, label: '1 day ago' },
{ value: Preset.ONE_WEEK_AGO, label: '1 week ago' },
{ value: Preset.ONE_MONTH_AGO, label: '1 month ago' },
],
serialize: ({ value, operator }) => {
let date = null;
switch (value) {
// Less than
case Preset.ONE_HOUR_AGO:
date = moment().subtract(1, 'hour');
break;
case Preset.ONE_DAY_AGO:
date = moment().subtract(1, 'day');
break;
case Preset.ONE_WEEK_AGO:
date = moment().subtract(1, 'week');
break;
case Preset.ONE_MONTH_AGO:
date = moment().subtract(1, 'month');
break;
}
let operatorSymbol = null;
switch (operator) {
case Operator.LESS_THAN:
operatorSymbol = '>';
break;
case Operator.MORE_THAN:
operatorSymbol = '<';
break;
}
if (date === null || operator === null)
return null;
return `${githubField}:${operatorSymbol}${date.format('YYYY-MM-DD')}`;
},
};
}
export const createdAt: IDropdownPredicate = makeDatePredicate('created at', 'Created At', 'created');
export const updatedAt: IDropdownPredicate = makeDatePredicate('updated at', 'Updated At', 'updated');
================================================
FILE: src/providers/github/predicates/draft.ts
================================================
import { IDropdownPredicate, PredicateType } from '../../types';
export const draft: IDropdownPredicate = {
name: 'draft',
label: 'Is Draft?',
type: PredicateType.DROPDOWN,
choices: [
{ value: 'false', label: 'No' },
{ value: 'true', label: 'Yes' },
],
operators: [],
serialize: ({ value }) => `draft:${value}`,
};
================================================
FILE: src/providers/github/predicates/index.ts
================================================
import { capitalize } from 'lodash';
import { Predicate, PredicateType } from '../../types';
import { mergeStatus } from './mergeStatus';
import { review } from './review';
import { status } from './status';
import { type } from './type';
import { draft } from "./draft";
import { createdAt, updatedAt } from './createdOrUpdatedAt';
enum GithubOperators {
EQUAL = 'equal',
NOT_EQUAL = 'not_equal',
}
interface ISimplePredicatePayload {
name: string;
placeholder: string;
label?: string;
}
/**
* Makes a simple text predicate
* @param options Options configuring the predicate
*/
export const makeSimplePredicate = ({
name,
label,
placeholder,
}: ISimplePredicatePayload): Predicate => ({
name,
label: label || capitalize(name),
placeholder,
type: PredicateType.TEXT,
operators: [
{ value: GithubOperators.EQUAL, label: '=' },
{ value: GithubOperators.NOT_EQUAL, label: '!=' },
],
serialize: ({ value, operator }) => {
const modifier = operator === GithubOperators.NOT_EQUAL ? '-' : '';
return `${modifier}${name}:"${value}"`;
},
});
export const availablePredicates: Predicate[] = [
makeSimplePredicate({
name: 'assignee',
label: 'Assignee',
placeholder: 'USERNAME',
}),
makeSimplePredicate({
name: 'author',
label: 'Author',
placeholder: 'USERNAME',
}),
makeSimplePredicate({
name: 'label',
label: 'Label',
placeholder: 'LABEL',
}),
makeSimplePredicate({
name: 'project',
label: 'Project',
placeholder: 'USERNAME/REPOSITORY/PROJECT',
}),
makeSimplePredicate({
name: 'mentions',
label: 'Mentions',
placeholder: 'USERNAME',
}),
makeSimplePredicate({
name: 'team',
label: 'Team',
placeholder: 'ORGNAME/TEAMNAME',
}),
makeSimplePredicate({
name: 'commenter',
label: 'Commenter',
placeholder: 'USERNAME',
}),
makeSimplePredicate({
name: 'involves',
label: 'Involves',
placeholder: 'USERNAME',
}),
makeSimplePredicate({
name: 'milestone',
label: 'Milestone',
placeholder: 'MILESTONE',
}),
makeSimplePredicate({
name: 'user',
label: 'User',
placeholder: 'USERNAME',
}),
makeSimplePredicate({
name: 'repo',
label: 'Repository',
placeholder: 'USERNAME/REPOSITORY',
}),
makeSimplePredicate({
name: 'org',
label: 'Organization',
placeholder: 'ORGNAME',
}),
makeSimplePredicate({
name: 'reviewed-by',
label: 'Reviewed by',
placeholder: 'USERNAME',
}),
makeSimplePredicate({
name: 'review-requested',
label: 'Requested Reviewer (User)',
placeholder: 'USERNAME',
}),
makeSimplePredicate({
name: 'team-review-requested',
label: 'Requested Reviewer (Team)',
placeholder: 'ORGNAME/TEAMNAME',
}),
type,
status,
mergeStatus,
review,
draft,
createdAt,
updatedAt
];
================================================
FILE: src/providers/github/predicates/mergeStatus.ts
================================================
import { IDropdownPredicate, PredicateType } from '../../types';
export const mergeStatus: IDropdownPredicate = {
name: 'merge status',
label: 'Merge Status',
type: PredicateType.DROPDOWN,
choices: [
{ value: 'merged', label: 'Merged' },
{ value: 'unmerged', label: 'Unmerged' },
],
operators: [],
serialize: ({ value }) => `is:${value}`,
};
================================================
FILE: src/providers/github/predicates/review.ts
================================================
import { IDropdownPredicate, PredicateType } from '../../types';
export const review: IDropdownPredicate = {
name: 'review',
label: 'Review Status',
type: PredicateType.DROPDOWN,
choices: [
{ value: 'none', label: 'None' },
{ value: 'required', label: 'Required' },
{ value: 'approved', label: 'Approved' },
{ value: 'changes_requested', label: 'Changes Requested' },
],
operators: [],
serialize: ({ value }) => `review:${value}`,
};
================================================
FILE: src/providers/github/predicates/status.ts
================================================
import { IDropdownPredicate, PredicateType } from '../../types';
export const status: IDropdownPredicate = {
name: 'status',
label: 'Status',
type: PredicateType.DROPDOWN,
choices: [
{ value: 'open', label: 'Open' },
{ value: 'closed', label: 'Closed' },
],
operators: [],
serialize: ({ value }) => `is:${value}`,
};
================================================
FILE: src/providers/github/predicates/type.ts
================================================
import { IDropdownPredicate, PredicateType } from '../../types';
export const type: IDropdownPredicate = {
name: 'type',
label: 'Type',
type: PredicateType.DROPDOWN,
choices: [
{ value: 'pr', label: 'PRs' },
{ value: 'issue', label: 'Issues' },
],
operators: [],
serialize: ({ value }) => `type:${value}`,
};
================================================
FILE: src/providers/index.ts
================================================
import { github } from './github';
import { jira } from './jira';
import { ProviderType } from './types';
export { AbstractProvider } from './AbstractProvider';
export * from './types';
export const providers = {
[ProviderType.GITHUB]: github,
[ProviderType.JIRA]: jira,
};
================================================
FILE: src/providers/jira/components/AvailableResources.tsx
================================================
import cx from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import { SettingsStore } from '../../../store/settings';
import { IJiraResource } from '../index';
interface IProps {
resources: IJiraResource[];
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const AvailableResources = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ resources, settingsStore }) => {
if (!resources || !resources.length) {
return null;
}
return (
<div className="mt-8">
<div className="font-medium mb-3">Available resources</div>
{resources.map((resource, index) => (
<div key={index} className="flex mb-8">
<div
className={cx(
'w-16 h-16 rounded-full overflow-hidden mr-3',
settingsStore.isDark ? 'bg-gray-700' : 'bg-gray-400'
)}
>
<img src={resource.avatarUrl} />
</div>
<div className="pt-2">
<div
className={
settingsStore.isDark ? 'text-gray-300' : 'text-gray-800'
}
>
{resource.name}
</div>
<div className="text-sm text-gray-600 mt-1">{resource.id}</div>
</div>
</div>
))}
</div>
);
});
================================================
FILE: src/providers/jira/components/IssueCard/IssueCard.tsx
================================================
import cx from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { compose } from 'recompose';
import * as timeago from 'timeago.js';
import { SettingsStore } from '../../../../store/settings';
import { JiraProvider } from '../../index';
import { StatusBadge } from './StatusBadge';
export interface IJiraIssue {
key: string;
fields: {
summary: string;
description: string;
created: string;
updated: string;
issuetype: {
name: string;
iconUrl: string;
};
project: {
name: string;
key: string;
avatarUrls: {
'48x48': string;
};
};
priority?: {
iconUrl: string;
name: string;
};
assignee: {
displayName: string;
emailAddress: string;
avatarUrls: {
'48x48': string;
};
};
status: {
name: string;
statusCategory: {
colorName: string;
};
};
};
}
export interface IProps {
provider: JiraProvider;
data: IJiraIssue;
isNew: boolean;
}
interface IInnerProps extends IProps {
settingsStore: SettingsStore;
}
export const IssueCard = compose<IInnerProps, IProps>(
inject('settingsStore'),
observer
)(({ data: issue, isNew, provider, settingsStore }) => {
// The API doesn't seem to return the URL to the actual issue on the web
// interface, so we use the one of the first resource. Note that for an
// account that has access to multiple resources, this will generate wrong
// URLs. Feel free to suggest a change if it doesn't work for you.
const url = `${provider.resources[0].url}/browse/${issue.key}`;
const linkStyle = settingsStore.isDark
? 'text-blue-400'
: 'text-blue-500 hover:text-blue-600';
return (
<div
className={cx(
'p-6 flex border-l-4',
isNew ? 'border-blue-500' : 'border-transparent'
)}
>
<div className="flex items-center justify-center flex-shrink-0 pr-4">
<div
className={cx(
'w-12 h-12 rounded-full overflow-hidden',
settingsStore.isDark ? 'bg-gray-700' : 'bg-gray-400'
)}
gitextract_cgx_ogxj/ ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── main.yml ├── .gitignore ├── .nvmrc ├── .parcelrc ├── .prettierrc ├── LICENSE ├── README.md ├── changelog.md ├── cypress/ │ ├── e2e/ │ │ ├── discover.spec.js │ │ ├── filters.spec.js │ │ └── settings.spec.js │ └── support/ │ ├── commands.js │ └── e2e.js ├── cypress.config.ts ├── jest.config.js ├── manifest.json ├── package.json ├── postcss.config.js ├── scripts/ │ ├── release │ └── screenshots ├── src/ │ ├── @types/ │ │ ├── contrast/ │ │ │ └── index.d.ts │ │ └── human-format/ │ │ └── index.d.ts │ ├── App.tsx │ ├── components/ │ │ ├── Button/ │ │ │ ├── Button.tsx │ │ │ └── index.ts │ │ ├── Dropdown/ │ │ │ ├── Dropdown.tsx │ │ │ └── index.ts │ │ ├── FilterLink/ │ │ │ ├── FilterLink.tsx │ │ │ └── index.ts │ │ ├── FilterPredicate/ │ │ │ ├── FilterPredicate.tsx │ │ │ ├── OperatorSelector.tsx │ │ │ ├── ValueSelector.tsx │ │ │ └── index.ts │ │ ├── Header/ │ │ │ ├── Header.tsx │ │ │ ├── TabLink.tsx │ │ │ └── index.ts │ │ ├── Loader/ │ │ │ ├── Loader.tsx │ │ │ └── index.ts │ │ ├── Modal/ │ │ │ ├── Modal.tsx │ │ │ └── index.ts │ │ ├── RadioCard/ │ │ │ ├── RadioCard.tsx │ │ │ └── index.ts │ │ ├── ToastManager/ │ │ │ ├── Toast.tsx │ │ │ ├── ToastManager.tsx │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── index.ts │ ├── constants/ │ │ ├── darkMode.ts │ │ ├── dates.ts │ │ └── languages.ts │ ├── containers/ │ │ ├── FilterEditModal/ │ │ │ ├── FilterEditModal.tsx │ │ │ ├── PredicatesStep.tsx │ │ │ ├── ProviderStep.tsx │ │ │ └── index.ts │ │ ├── RepoCard/ │ │ │ ├── RepoCard.tsx │ │ │ └── index.ts │ │ ├── SettingsModal/ │ │ │ ├── Panel.tsx │ │ │ ├── SettingsModal.tsx │ │ │ ├── Sidebar.tsx │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ ├── tabs/ │ │ │ │ ├── Cache.tsx │ │ │ │ ├── NightMode.tsx │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ └── index.ts │ ├── errors/ │ │ ├── InvalidCredentials.ts │ │ ├── NeedTokenError.ts │ │ ├── RateLimitError.ts │ │ └── index.ts │ ├── index.html │ ├── index.scss │ ├── index.tsx │ ├── lib/ │ │ ├── assertUnreachable.ts │ │ ├── cache.ts │ │ └── github/ │ │ ├── index.ts │ │ └── trending/ │ │ └── index.ts │ ├── migrations/ │ │ ├── index.ts │ │ ├── mocks/ │ │ │ ├── index.ts │ │ │ ├── v0.ts │ │ │ ├── v1-without-token.ts │ │ │ ├── v1.ts │ │ │ ├── v2-with-negated-predicates.ts │ │ │ ├── v2.ts │ │ │ └── v3-with-defaulted-operators.ts │ │ ├── testing-utils.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ ├── v0-to-v1.test.ts │ │ ├── v0-to-v1.ts │ │ ├── v1-to-v2.test.ts │ │ ├── v1-to-v2.ts │ │ ├── v2-to-v3.test.ts │ │ └── v2-to-v3.ts │ ├── pages/ │ │ ├── Dashboard/ │ │ │ ├── Dashboard.tsx │ │ │ ├── FilterLinkContainer.tsx │ │ │ └── index.ts │ │ ├── Discover/ │ │ │ ├── Discover.scss │ │ │ ├── Discover.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── providers/ │ │ ├── AbstractProvider.ts │ │ ├── github/ │ │ │ ├── components/ │ │ │ │ ├── IssueCard/ │ │ │ │ │ ├── CheckStatusIndicator.tsx │ │ │ │ │ ├── ConflictIndicator.tsx │ │ │ │ │ ├── ContextualDropdown.tsx │ │ │ │ │ ├── IssueCard.tsx │ │ │ │ │ ├── IssueStatusIndicator.tsx │ │ │ │ │ ├── LabelBadge.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── ProfileCard.tsx │ │ │ │ └── Settings.tsx │ │ │ ├── fetchers/ │ │ │ │ ├── client.ts │ │ │ │ ├── graphql/ │ │ │ │ │ ├── query.ts │ │ │ │ │ ├── search.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── index.ts │ │ │ │ └── rest/ │ │ │ │ ├── profile.ts │ │ │ │ └── search.ts │ │ │ ├── index.tsx │ │ │ └── predicates/ │ │ │ ├── createdOrUpdatedAt.ts │ │ │ ├── draft.ts │ │ │ ├── index.ts │ │ │ ├── mergeStatus.ts │ │ │ ├── review.ts │ │ │ ├── status.ts │ │ │ └── type.ts │ │ ├── index.ts │ │ ├── jira/ │ │ │ ├── components/ │ │ │ │ ├── AvailableResources.tsx │ │ │ │ ├── IssueCard/ │ │ │ │ │ ├── IssueCard.tsx │ │ │ │ │ ├── StatusBadge.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── LoginButton.tsx │ │ │ │ ├── LogoutButton.tsx │ │ │ │ └── Settings.tsx │ │ │ ├── fetchers/ │ │ │ │ ├── index.ts │ │ │ │ ├── refreshToken.ts │ │ │ │ ├── resources.ts │ │ │ │ └── swapToken.ts │ │ │ ├── index.tsx │ │ │ └── predicates/ │ │ │ └── index.ts │ │ └── types.ts │ ├── service_worker/ │ │ ├── cache.js │ │ └── index.js │ ├── setupTests.ts │ └── store/ │ ├── filters.ts │ ├── index.ts │ ├── models/ │ │ └── filter.ts │ ├── navigation.ts │ ├── settings.ts │ └── trends.ts ├── tailwind.config.js └── tsconfig.json
SYMBOL INDEX (263 symbols across 81 files)
FILE: cypress.config.ts
method setupNodeEvents (line 11) | setupNodeEvents(on, config) {}
FILE: cypress/e2e/filters.spec.js
constant DEFAULT_FILTER_NAME (line 1) | const DEFAULT_FILTER_NAME = 'OctoLenses Issues';
function createFilter (line 92) | function createFilter({ name, predicates }) {
FILE: src/App.tsx
constant PAGES (line 9) | const PAGES = {
type PageName (line 14) | type PageName = keyof typeof PAGES;
type IInnerProps (line 16) | interface IInnerProps {
FILE: src/components/Button/Button.tsx
type ButtonType (line 4) | enum ButtonType {
constant TYPE_TO_CLASSES (line 9) | const TYPE_TO_CLASSES: Record<ButtonType, string> = {
type IProps (line 14) | interface IProps {
FILE: src/components/Dropdown/Dropdown.tsx
type IProps (line 8) | interface IProps {
type IInnerProps (line 16) | interface IInnerProps extends IProps {
type IOption (line 20) | interface IOption {
function handleChange (line 29) | function handleChange(event: ChangeEvent<HTMLSelectElement>) {
FILE: src/components/FilterLink/FilterLink.tsx
type IProps (line 27) | interface IProps extends SortableElementProps {
type IInnerProps (line 33) | interface IInnerProps extends IProps {
FILE: src/components/FilterPredicate/FilterPredicate.tsx
type IProps (line 25) | interface IProps {
type IInnerProps (line 34) | interface IInnerProps extends IProps {
FILE: src/components/FilterPredicate/OperatorSelector.tsx
type IProps (line 5) | interface IProps {
FILE: src/components/FilterPredicate/ValueSelector.tsx
type IProps (line 9) | interface IProps {
type IInnerProps (line 15) | interface IInnerProps extends IProps {
FILE: src/components/Header/Header.tsx
type IInnerProps (line 12) | interface IInnerProps {
function renderLink (line 23) | function renderLink(name: string) {
FILE: src/components/Header/TabLink.tsx
constant COLORS (line 8) | const COLORS = {
type IProps (line 19) | interface IProps {
type IInnerProps (line 25) | interface IInnerProps extends IProps {
FILE: src/components/Loader/Loader.tsx
type IProps (line 8) | interface IProps {
type IInnerProps (line 14) | interface IInnerProps extends IProps {
FILE: src/components/Modal/Modal.tsx
type IProps (line 27) | interface IProps {
type IInnerProps (line 33) | interface IInnerProps extends IProps {
function handleKeyDown (line 43) | function handleKeyDown(event: KeyboardEvent) {
FILE: src/components/RadioCard/RadioCard.tsx
type IProps (line 8) | interface IProps {
type IInnerProps (line 17) | interface IInnerProps extends IProps {
constant COLORS (line 21) | const COLORS = {
FILE: src/components/ToastManager/Toast.tsx
constant TYPES_TO_ICON (line 7) | const TYPES_TO_ICON = {
constant TYPES_TO_THEME (line 12) | const TYPES_TO_THEME = {
constant TOAST_DURATION (line 17) | const TOAST_DURATION = 3000;
constant TOAST_FADE_DURATION (line 18) | const TOAST_FADE_DURATION = 200;
type IProps (line 26) | interface IProps extends INotification {
class Toast (line 30) | class Toast extends React.Component<IProps> {
method componentDidMount (line 35) | public componentDidMount() {
method render (line 49) | public render() {
FILE: src/components/ToastManager/ToastManager.tsx
type IState (line 14) | interface IState {
class ToastManager (line 18) | class ToastManager extends React.Component<{}, IState> {
method componentDidMount (line 23) | public componentDidMount() {
method addNotification (line 27) | public addNotification(message: string, type: NotificationType) {
method render (line 47) | public render() {
function toast (line 68) | function toast(message: string, type: NotificationType) {
FILE: src/components/ToastManager/types.ts
type INotification (line 1) | interface INotification {
type NotificationType (line 7) | type NotificationType = 'info' | 'error';
FILE: src/constants/darkMode.ts
constant DARK_MODE (line 1) | const DARK_MODE = {
FILE: src/constants/dates.ts
type DateType (line 4) | type DateType =
type IDatePreset (line 12) | interface IDatePreset {
constant DATES (line 21) | const DATES: IDatePreset[] = [
FILE: src/constants/languages.ts
constant LANGUAGES (line 1) | const LANGUAGES = [
FILE: src/containers/FilterEditModal/FilterEditModal.tsx
type STEPS (line 18) | enum STEPS {
type IProps (line 23) | interface IProps {
type IInnerProps (line 28) | interface IInnerProps extends IProps {
function handleSave (line 46) | function handleSave() {
function defaultFilter (line 84) | function defaultFilter(filter?: Filter) {
FILE: src/containers/FilterEditModal/PredicatesStep.tsx
type IProps (line 12) | interface IProps {
type IInnerProps (line 22) | interface IInnerProps extends IProps {
function handleKeyDown (line 42) | function handleKeyDown(event: KeyboardEvent) {
function handleAddPredicate (line 55) | function handleAddPredicate(event: ChangeEvent<HTMLSelectElement>) {
FILE: src/containers/FilterEditModal/ProviderStep.tsx
type IProps (line 7) | interface IProps {
FILE: src/containers/RepoCard/RepoCard.tsx
type IProps (line 11) | interface IProps {
type IInnerProps (line 15) | interface IInnerProps extends IProps {
FILE: src/containers/SettingsModal/Panel.tsx
type IProps (line 9) | interface IProps {
type IInnerProps (line 13) | interface IInnerProps extends IProps {
FILE: src/containers/SettingsModal/SettingsModal.tsx
type IProps (line 13) | interface IProps {
function SettingsModal (line 17) | function SettingsModal({ onClose }: IProps) {
FILE: src/containers/SettingsModal/Sidebar.tsx
type IProps (line 35) | interface IProps {
type IInnerProps (line 40) | interface IInnerProps extends IProps {
function renderItems (line 50) | function renderItems(items: ISettingView[]) {
FILE: src/containers/SettingsModal/constants.ts
constant SETTINGS_VIEWS (line 7) | const SETTINGS_VIEWS: ISettingView[] = [
FILE: src/containers/SettingsModal/tabs/Cache.tsx
function flushCache (line 8) | function flushCache() {
FILE: src/containers/SettingsModal/tabs/NightMode.tsx
type IProps (line 7) | interface IProps {
FILE: src/containers/SettingsModal/types.ts
type ISettingView (line 1) | interface ISettingView {
FILE: src/errors/InvalidCredentials.ts
class InvalidCredentials (line 3) | class InvalidCredentials extends ExtendableError {
method constructor (line 4) | constructor(message = 'Your token seems to be invalid') {
FILE: src/errors/NeedTokenError.ts
class NeedTokenError (line 3) | class NeedTokenError extends ExtendableError {
method constructor (line 4) | constructor(message = 'Fetching failed. Try to set a Github token') {
FILE: src/errors/RateLimitError.ts
class RateLimitError (line 3) | class RateLimitError extends ExtendableError {
method constructor (line 6) | constructor(remainingRateLimit: number) {
FILE: src/lib/assertUnreachable.ts
function assertUnreachable (line 1) | function assertUnreachable<T>(_: never, defaultValue: T): T {
FILE: src/lib/cache.ts
type ICacheEntry (line 3) | interface ICacheEntry<T> {
class Cache (line 8) | class Cache {
method remember (line 18) | public static async remember<T = any>(
method get (line 42) | public static get<T = any>(key: string): ICacheEntry<T> {
method put (line 60) | public static put(key: string, value: any, lifespan: number) {
method forget (line 74) | public static forget(key: string) {
method flush (line 81) | public static flush() {
method flushExpired (line 88) | public static flushExpired() {
method getPrefixedKey (line 98) | private static getPrefixedKey(key: string) {
method isPrefixed (line 110) | private static isPrefixed(key: string) {
method getCacheKeys (line 117) | private static getCacheKeys() {
FILE: src/lib/github/trending/index.ts
type IFetchTrendingReposParams (line 6) | interface IFetchTrendingReposParams {
FILE: src/migrations/index.ts
class Migrator (line 10) | class Migrator {
method migrate (line 13) | public migrate() {
method registerMigration (line 24) | public registerMigration(migration: IMigration): Migrator {
FILE: src/migrations/testing-utils.ts
type Window (line 11) | interface Window {
FILE: src/migrations/types.ts
type IMigration (line 1) | interface IMigration {
FILE: src/migrations/utils.ts
function getFromLocalStorage (line 1) | function getFromLocalStorage(key: string) {
function saveToLocalStorage (line 11) | function saveToLocalStorage(key: string, value: any) {
function hydrateLocalStorageFromObject (line 16) | function hydrateLocalStorageFromObject(object: any) {
function dumpLocalStorageToObject (line 24) | function dumpLocalStorageToObject() {
FILE: src/migrations/v0-to-v1.ts
method shouldRun (line 9) | public shouldRun(): boolean {
method run (line 19) | public run() {
FILE: src/migrations/v1-to-v2.ts
method shouldRun (line 11) | public shouldRun(): boolean {
method run (line 21) | public run() {
method defaultFiltersProviderToGithub (line 27) | private defaultFiltersProviderToGithub() {
method moveTokenFromSettingsToGithubProvider (line 38) | private moveTokenFromSettingsToGithubProvider() {
method upgradeSchemaVersion (line 52) | private upgradeSchemaVersion() {
FILE: src/migrations/v2-to-v3.ts
method shouldRun (line 11) | public shouldRun(): boolean {
method run (line 21) | public run() {
method defaultPredicatesOperators (line 26) | private defaultPredicatesOperators() {
method upgradeSchemaVersion (line 58) | private upgradeSchemaVersion() {
FILE: src/pages/Dashboard/Dashboard.tsx
type IProps (line 15) | interface IProps {
class Dashboard (line 22) | class Dashboard extends React.Component<IProps> {
method componentDidMount (line 28) | componentDidMount() {
method componentWillUnmount (line 33) | componentWillUnmount() {
method selectedFilter (line 51) | get selectedFilter() {
method render (line 152) | public render() {
method renderResults (line 240) | public renderResults() {
FILE: src/pages/Dashboard/FilterLinkContainer.tsx
type IProps (line 7) | interface IProps {
FILE: src/pages/Discover/Discover.tsx
type IInnerProps (line 14) | interface IInnerProps {
function handleChangeLanguage (line 23) | function handleChangeLanguage({ value }: { value: string }) {
function handleChangeDateRange (line 28) | function handleChangeDateRange({ value }: { value: string }) {
FILE: src/providers/AbstractProvider.ts
type ISettingsComponentProps (line 8) | interface ISettingsComponentProps {
type SettingsComponent (line 12) | type SettingsComponent = ({ settings }: ISettingsComponentProps) => JSX....
type CardComponent (line 13) | type CardComponent = React.ComponentType<{ data: any }>;
FILE: src/providers/github/components/IssueCard/CheckStatusIndicator.tsx
type IProps (line 7) | interface IProps {
constant STATUS_TO_ICON (line 11) | const STATUS_TO_ICON = {
constant STATUS_TO_LABEL (line 17) | const STATUS_TO_LABEL = {
FILE: src/providers/github/components/IssueCard/ConflictIndicator.tsx
type IProps (line 3) | interface IProps {
FILE: src/providers/github/components/IssueCard/ContextualDropdown.tsx
type IProps (line 58) | interface IProps {
type IInnerProps (line 62) | interface IInnerProps extends IProps {
FILE: src/providers/github/components/IssueCard/IssueCard.tsx
type IIssue (line 16) | interface IIssue {
type IProps (line 49) | interface IProps {
type IInnerProps (line 54) | interface IInnerProps extends IProps {
function getTotalCommentsCount (line 68) | function getTotalCommentsCount() {
function getLastActivityDate (line 74) | function getLastActivityDate(item: TimelineItem) {
FILE: src/providers/github/components/IssueCard/IssueStatusIndicator.tsx
type IProps (line 7) | interface IProps {
function getColor (line 23) | function getColor(state: IIssue['state'], isDraft: boolean)
FILE: src/providers/github/components/IssueCard/LabelBadge.tsx
type IProps (line 5) | interface IProps {
FILE: src/providers/github/components/IssueCard/types.ts
type IssueStatus (line 1) | enum IssueStatus {
type TimelineItemType (line 8) | enum TimelineItemType {
type TimelineItem (line 14) | type TimelineItem =
type IIssueComment (line 19) | interface IIssueComment {
type IPullRequestCommit (line 24) | interface IPullRequestCommit {
type IPullRequestReview (line 31) | interface IPullRequestReview {
FILE: src/providers/github/components/ProfileCard.tsx
type IProps (line 9) | interface IProps {
type IInnerProps (line 13) | interface IInnerProps extends IProps {
FILE: src/providers/github/components/Settings.tsx
constant CREATE_TOKEN_URL (line 13) | const CREATE_TOKEN_URL =
type IProps (line 23) | interface IProps {
type IInnerProps (line 27) | interface IInnerProps extends IProps {
function handleSubmit (line 37) | function handleSubmit() {
FILE: src/providers/github/fetchers/client.ts
type IClientParams (line 9) | interface IClientParams {
FILE: src/providers/github/fetchers/graphql/utils.ts
constant COMMIT_STATUS_TO_STATUS (line 5) | const COMMIT_STATUS_TO_STATUS: Record<any, IssueStatus> = {
constant CHECK_CONCLUSION_TO_STATUS (line 13) | const CHECK_CONCLUSION_TO_STATUS: Record<any, IssueStatus> = {
function extractGraphqlStatus (line 22) | function extractGraphqlStatus(issue: any) {
function extractGraphqlLabels (line 45) | function extractGraphqlLabels(issue: any) {
function extractConflictStatus (line 49) | function extractConflictStatus(issue: any) {
FILE: src/providers/github/index.tsx
type IGithubSettings (line 13) | interface IGithubSettings {
type IGithubProfile (line 17) | interface IGithubProfile {
class GithubProvider (line 24) | class GithubProvider extends AbstractProvider<IGithubSettings> {
method initialize (line 34) | public async initialize() {
method fetchFilter (line 38) | public async fetchFilter(filter: Filter) {
method resolveFilterItemIdentifier (line 42) | public resolveFilterItemIdentifier(item: any) {
method findPredicate (line 48) | public findPredicate(name: string) {
method fetchProfile (line 53) | public async fetchProfile() {
method setToken (line 62) | public setToken(token: string) {
FILE: src/providers/github/predicates/createdOrUpdatedAt.ts
type Preset (line 4) | enum Preset {
type Operator (line 11) | enum Operator {
function makeDatePredicate (line 16) | function makeDatePredicate(name: string, label: string, githubField: str...
FILE: src/providers/github/predicates/index.ts
type GithubOperators (line 11) | enum GithubOperators {
type ISimplePredicatePayload (line 16) | interface ISimplePredicatePayload {
FILE: src/providers/jira/components/AvailableResources.tsx
type IProps (line 9) | interface IProps {
type IInnerProps (line 13) | interface IInnerProps extends IProps {
FILE: src/providers/jira/components/IssueCard/IssueCard.tsx
type IJiraIssue (line 11) | interface IJiraIssue {
type IProps (line 49) | interface IProps {
type IInnerProps (line 55) | interface IInnerProps extends IProps {
FILE: src/providers/jira/components/IssueCard/StatusBadge.tsx
type IProps (line 6) | interface IProps {
type JiraColor (line 10) | type JiraColor = 'green' | 'yellow' | 'blue-gray';
constant COLORS_TO_STYLE (line 12) | const COLORS_TO_STYLE: Record<JiraColor, string> = {
FILE: src/providers/jira/components/LoginButton.tsx
constant CLIENT_ID (line 12) | const CLIENT_ID = '4WgiRI4XRQ2OTWof5i7yCKmlekkIldH0';
type IProps (line 14) | interface IProps {
type IInnerProps (line 18) | interface IInnerProps extends IProps {
function handleLogin (line 26) | async function handleLogin() {
function initJiraOauthFlow (line 87) | async function initJiraOauthFlow(): Promise<ISwapResult> {
FILE: src/providers/jira/components/LogoutButton.tsx
type IProps (line 7) | interface IProps {
function handleLogout (line 12) | function handleLogout() {
FILE: src/providers/jira/components/Settings.tsx
type IProps (line 9) | interface IProps {
FILE: src/providers/jira/fetchers/index.ts
function fetchFilter (line 8) | async function fetchFilter(
FILE: src/providers/jira/fetchers/refreshToken.ts
type IRefreshResult (line 1) | interface IRefreshResult {
FILE: src/providers/jira/fetchers/swapToken.ts
type ISwapResult (line 1) | interface ISwapResult {
FILE: src/providers/jira/index.tsx
constant FIVE_MINUTES (line 19) | const FIVE_MINUTES = 5 * 60 * 1000;
type IJiraSettings (line 21) | interface IJiraSettings {
type IJiraResource (line 29) | interface IJiraResource {
class JiraProvider (line 37) | class JiraProvider extends AbstractProvider<IJiraSettings> {
method initialize (line 48) | public async initialize() {
method fetchFilter (line 56) | public async fetchFilter(filter: Filter) {
method resolveFilterItemIdentifier (line 60) | public resolveFilterItemIdentifier(item: any): string {
method findPredicate (line 66) | public findPredicate(name: string) {
method shouldRefreshToken (line 71) | private get shouldRefreshToken() {
method refreshToken (line 78) | public async refreshToken() {
method setAuth (line 85) | public setAuth({ access_token, expires_in, refresh_token }: ISwapResul...
method disconnect (line 96) | public disconnect() {
method fetchResources (line 102) | public async fetchResources() {
FILE: src/providers/jira/predicates/index.ts
type JiraOperators (line 5) | enum JiraOperators {
type ISimplePredicatePayload (line 18) | interface ISimplePredicatePayload {
FILE: src/providers/types.ts
type ProviderType (line 1) | enum ProviderType {
type PredicateIdentifier (line 6) | type PredicateIdentifier = string;
type IBasePredicate (line 9) | interface IBasePredicate {
type ITextPredicate (line 18) | interface ITextPredicate extends IBasePredicate {
type IDropdownPredicate (line 24) | interface IDropdownPredicate extends IBasePredicate {
type Predicate (line 29) | type Predicate = ITextPredicate | IDropdownPredicate;
type IStoredPredicate (line 32) | interface IStoredPredicate {
type PredicateType (line 38) | enum PredicateType {
type IPredicateOperator (line 43) | interface IPredicateOperator {
FILE: src/service_worker/cache.js
constant PRECACHE (line 4) | const PRECACHE = 'precache-v1';
constant RUNTIME (line 5) | const RUNTIME = 'runtime';
constant PRECACHE_URLS (line 8) | const PRECACHE_URLS = [
FILE: src/service_worker/index.js
function openInNewTab (line 3) | function openInNewTab() {
FILE: src/store/filters.ts
class FiltersStore (line 12) | class FiltersStore {
method count (line 18) | get count() {
method findFilter (line 22) | public findFilter(id: string) {
method findFilterIndex (line 26) | public findFilterIndex(id: string) {
method getFilters (line 30) | public getFilters() {
method getFilterAt (line 34) | public getFilterAt(index: number) {
method getFirstFilter (line 38) | public getFirstFilter() {
method saveFilter (line 44) | public saveFilter(filterPayload: any) {
method cloneFilter (line 62) | public cloneFilter(id: FilterIdentifier) {
method removeFilter (line 74) | public removeFilter(id: FilterIdentifier) {
method swapFilters (line 80) | public swapFilters(oldIndex: number, newIndex: number) {
method fetchAllFilters (line 84) | public async fetchAllFilters() {
constant EMPTY_FILTER_PAYLOAD (line 89) | const EMPTY_FILTER_PAYLOAD = {
FILE: src/store/models/filter.ts
type FilterIdentifier (line 10) | type FilterIdentifier = string;
class Filter (line 12) | class Filter {
method constructor (line 51) | constructor() {
method fromAttributes (line 65) | public static fromAttributes({ id, ...otherAttributes }: any) {
method serializePredicate (line 76) | public serializePredicate(payload: IStoredPredicate): string {
method clone (line 82) | public clone(): Filter {
method isItemNew (line 96) | public isItemNew(item: any) {
method clearNewItemsNotifications (line 101) | public clearNewItemsNotifications() {
method update (line 105) | public update(payload: any) {
method hash (line 117) | public get hash(): string {
method newItemsCount (line 126) | public get newItemsCount() {
method fetchFilter (line 135) | public async fetchFilter() {
method invalidateCache (line 151) | public invalidateCache() {
method setData (line 156) | public setData(data: any) {
method getItemsIdentifiers (line 177) | private getItemsIdentifiers(items: any[]) {
FILE: src/store/navigation.ts
class NavigationStore (line 4) | class NavigationStore {
method navigateTo (line 10) | public navigateTo(newPage: string) {
FILE: src/store/settings.ts
class SettingsStore (line 8) | class SettingsStore {
method applyDarkMode (line 62) | public applyDarkMode() {
method updateDarkMode (line 78) | public updateDarkMode(darkMode: string) {
method updateWasOnboarded (line 83) | public updateWasOnboarded(wasOnboarded: boolean) {
method updateLanguage (line 88) | public updateLanguage(language: string) {
method updateDateRange (line 93) | public updateDateRange(dateRange: DateType) {
FILE: src/store/trends.ts
class TrendsStore (line 7) | class TrendsStore {
method updateRepos (line 15) | public updateRepos(newRepos: any[]) {
method setLoading (line 20) | public setLoading(isLoading: boolean) {
method fetchTrendingRepos (line 25) | public async fetchTrendingRepos() {
Condensed preview — 159 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (234K chars).
[
{
"path": ".babelrc",
"chars": 85,
"preview": "{\n \"plugins\": [\n [\"@babel/plugin-proposal-decorators\", { \"legacy\": true }]\n ]\n}\n"
},
{
"path": ".editorconfig",
"chars": 143,
"preview": "root=true\n\n[*]\nend_of_line = lf\ntab_width = 2\nindent_style = space\ninsert_final_newline = true\ncharset = utf-8\ntrim_trai"
},
{
"path": ".eslintignore",
"chars": 27,
"preview": ".cache/\ndist/\nnode_modules/"
},
{
"path": ".eslintrc",
"chars": 1617,
"preview": "{\n \"extends\": [\n // Enable eslint recommended, and the few overrides necessary to work with TS\n \"eslint:recommend"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 612,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\n\n---\n\n**Describe the bug**\nA clear and concise descriptio"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 560,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\n\n---\n\n**Is your feature request related to a problem? "
},
{
"path": ".github/workflows/main.yml",
"chars": 1071,
"preview": "name: Lint & Tests\non: [push]\njobs:\n lint:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: a"
},
{
"path": ".gitignore",
"chars": 116,
"preview": "node_modules/\n\n.idea/\n\nyarn-error.log\n\n.cache/\n.parcel-cache/\n\ndist/\ndist.crx\ndist.pem\noctolenses-*\n\ncypress/videos\n"
},
{
"path": ".nvmrc",
"chars": 3,
"preview": "20\n"
},
{
"path": ".parcelrc",
"chars": 123,
"preview": "{\n \"extends\": \"@parcel/config-default\",\n \"transformers\": {\n \"*.{ts,tsx}\": [\"@parcel/transformer-typescript-tsc\"]\n "
},
{
"path": ".prettierrc",
"chars": 52,
"preview": "{\n \"singleQuote\": true,\n \"trailingComma\": \"es5\"\n}\n"
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2018 Renan GEHAN\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 4817,
"preview": "[](https://chrome.google.com/webstore/detail/octolenses/ghlblfakaklgkdmfejdlffbmpcaidoci)\n["
},
{
"path": "changelog.md",
"chars": 21165,
"preview": "### v2.3.0\n- Allow filtering PRs/issues by relative update/creation date [Renan GEHAN]\n\n### v2.2.2\n- Refresh all button,"
},
{
"path": "cypress/e2e/discover.spec.js",
"chars": 1355,
"preview": "context('Discover', () => {\n beforeEach(() => {\n cy.injectGithubToken();\n\n cy.visit(Cypress.env('BASE_URL') + '/'"
},
{
"path": "cypress/e2e/filters.spec.js",
"chars": 3244,
"preview": "const DEFAULT_FILTER_NAME = 'OctoLenses Issues';\n\ncontext('Filters', () => {\n beforeEach(() => {\n cy.injectGithubTok"
},
{
"path": "cypress/e2e/settings.spec.js",
"chars": 971,
"preview": "context('Discover', () => {\n beforeEach(() => {\n cy.visit(Cypress.env('BASE_URL') + '/');\n\n cy.get('[data-header-"
},
{
"path": "cypress/support/commands.js",
"chars": 410,
"preview": "Cypress.Commands.add('injectGithubToken', () => {\n cy.window().then(window => {\n const token = Cypress.env('GITHUB_T"
},
{
"path": "cypress/support/e2e.js",
"chars": 21,
"preview": "import './commands';\n"
},
{
"path": "cypress.config.ts",
"chars": 307,
"preview": "import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n projectId: 'r2fbf6',\n watchForFileChanges: fals"
},
{
"path": "jest.config.js",
"chars": 242,
"preview": "module.exports = {\n preset: 'ts-jest/presets/js-with-ts',\n testMatch: ['<rootDir>/src/**/*.test.ts'],\n testEnvironmen"
},
{
"path": "manifest.json",
"chars": 1278,
"preview": "{\n \"key\": \"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq/S9dl/VwNTcp+T5ZLnOH8gOZoMkFTiKNsR6EDv3zUiEImaheCDMzFUhRiyxg1NY"
},
{
"path": "package.json",
"chars": 3248,
"preview": "{\n \"name\": \"github-trending-chrome-extension\",\n \"targets\": {\n \"app\": {\n \"context\": \"browser\",\n \"distDir\":"
},
{
"path": "postcss.config.js",
"chars": 152,
"preview": "const tailwindcss = require('tailwindcss');\n\n// prettier-ignore\nmodule.exports = {\n plugins: [\n tailwindcss(),\n r"
},
{
"path": "scripts/release",
"chars": 1909,
"preview": "#!/usr/bin/env node\n\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst { readFileSync, "
},
{
"path": "scripts/screenshots",
"chars": 3972,
"preview": "#!/usr/bin/env node\n\nconst path = require('path');\n\nconst Bundler = require('parcel-bundler');\nconst puppeteer = require"
},
{
"path": "src/@types/contrast/index.d.ts",
"chars": 27,
"preview": "declare module 'contrast';\n"
},
{
"path": "src/@types/human-format/index.d.ts",
"chars": 31,
"preview": "declare module 'human-format';\n"
},
{
"path": "src/App.tsx",
"chars": 716,
"preview": "import { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport {"
},
{
"path": "src/components/Button/Button.tsx",
"chars": 842,
"preview": "import cx from 'classnames';\nimport React, { ReactNode } from 'react';\n\nexport enum ButtonType {\n PRIMARY = 'primary',\n"
},
{
"path": "src/components/Button/index.ts",
"chars": 47,
"preview": "export { Button, ButtonType } from './Button';\n"
},
{
"path": "src/components/Dropdown/Dropdown.tsx",
"chars": 1491,
"preview": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React, { ChangeEvent } from 'react';\n"
},
{
"path": "src/components/Dropdown/index.ts",
"chars": 39,
"preview": "export { Dropdown } from './Dropdown';\n"
},
{
"path": "src/components/FilterLink/FilterLink.tsx",
"chars": 2341,
"preview": "import cx from 'classnames';\nimport { size } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport React "
},
{
"path": "src/components/FilterLink/index.ts",
"chars": 43,
"preview": "export { FilterLink } from './FilterLink';\n"
},
{
"path": "src/components/FilterPredicate/FilterPredicate.tsx",
"chars": 2398,
"preview": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose "
},
{
"path": "src/components/FilterPredicate/OperatorSelector.tsx",
"chars": 668,
"preview": "import React from 'react';\n\nimport { Predicate } from '../../providers';\n\ninterface IProps {\n predicate: Predicate;\n v"
},
{
"path": "src/components/FilterPredicate/ValueSelector.tsx",
"chars": 1584,
"preview": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose "
},
{
"path": "src/components/FilterPredicate/index.ts",
"chars": 53,
"preview": "export { FilterPredicate } from './FilterPredicate';\n"
},
{
"path": "src/components/Header/Header.tsx",
"chars": 1853,
"preview": "import cx from 'classnames';\nimport { capitalize } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport "
},
{
"path": "src/components/Header/TabLink.tsx",
"chars": 975,
"preview": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose "
},
{
"path": "src/components/Header/index.ts",
"chars": 35,
"preview": "export { Header } from './Header';\n"
},
{
"path": "src/components/Loader/Loader.tsx",
"chars": 1457,
"preview": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose "
},
{
"path": "src/components/Loader/index.ts",
"chars": 35,
"preview": "export { Loader } from './Loader';\n"
},
{
"path": "src/components/Modal/Modal.tsx",
"chars": 1879,
"preview": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React, { ReactNode, useEffect } from "
},
{
"path": "src/components/Modal/index.ts",
"chars": 33,
"preview": "export { Modal } from './Modal';\n"
},
{
"path": "src/components/RadioCard/RadioCard.tsx",
"chars": 1564,
"preview": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose "
},
{
"path": "src/components/RadioCard/index.ts",
"chars": 41,
"preview": "export { RadioCard } from './RadioCard';\n"
},
{
"path": "src/components/ToastManager/Toast.tsx",
"chars": 1491,
"preview": "import cx from 'classnames';\nimport React from 'react';\nimport styled from 'styled-components';\n\nimport { INotification "
},
{
"path": "src/components/ToastManager/ToastManager.tsx",
"chars": 1813,
"preview": "import { reject } from 'lodash';\nimport React from 'react';\n\nimport { Toast } from './Toast';\nimport { INotification, No"
},
{
"path": "src/components/ToastManager/index.ts",
"chars": 54,
"preview": "export { ToastManager, toast } from './ToastManager';\n"
},
{
"path": "src/components/ToastManager/types.ts",
"chars": 144,
"preview": "export interface INotification {\n id: string;\n message: string;\n type: NotificationType;\n}\n\nexport type NotificationT"
},
{
"path": "src/components/index.ts",
"chars": 199,
"preview": "export { Dropdown } from './Dropdown';\nexport { Loader } from './Loader';\nexport { Header } from './Header';\nexport { Fi"
},
{
"path": "src/constants/darkMode.ts",
"chars": 100,
"preview": "export const DARK_MODE = {\n DISABLED: 'DISABLED',\n ENABLED: 'ENABLED',\n AT_NIGHT: 'AT_NIGHT',\n};\n"
},
{
"path": "src/constants/dates.ts",
"chars": 1190,
"preview": "import { find } from 'lodash';\nimport moment from 'moment';\n\nexport type DateType =\n | 'last_week'\n | 'last_two_weeks'"
},
{
"path": "src/constants/languages.ts",
"chars": 19989,
"preview": "export const LANGUAGES = [\n { value: null, name: 'All Languages' },\n { value: '1c-enterprise', name: '1C Enterprise' }"
},
{
"path": "src/containers/FilterEditModal/FilterEditModal.tsx",
"chars": 2368,
"preview": "import { pick } from 'lodash';\nimport { toJS } from 'mobx';\nimport { inject, observer } from 'mobx-react';\nimport React,"
},
{
"path": "src/containers/FilterEditModal/PredicatesStep.tsx",
"chars": 4447,
"preview": "import cx from 'classnames';\nimport { chain, get } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport "
},
{
"path": "src/containers/FilterEditModal/ProviderStep.tsx",
"chars": 1391,
"preview": "import React from 'react';\n\nimport { Button, ButtonType } from '../../components/Button';\nimport { RadioCard } from '../"
},
{
"path": "src/containers/FilterEditModal/index.ts",
"chars": 53,
"preview": "export { FilterEditModal } from './FilterEditModal';\n"
},
{
"path": "src/containers/RepoCard/RepoCard.tsx",
"chars": 2562,
"preview": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport cx from 'classnames';\nimport humanFormat from 'human-format';\n"
},
{
"path": "src/containers/RepoCard/index.ts",
"chars": 39,
"preview": "export { RepoCard } from './RepoCard';\n"
},
{
"path": "src/containers/SettingsModal/Panel.tsx",
"chars": 708,
"preview": "import { find } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compos"
},
{
"path": "src/containers/SettingsModal/SettingsModal.tsx",
"chars": 704,
"preview": "import React, { useState } from 'react';\nimport styled from 'styled-components';\n\nimport { Modal } from '../../component"
},
{
"path": "src/containers/SettingsModal/Sidebar.tsx",
"chars": 1850,
"preview": "import cx from 'classnames';\nimport { partition } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport R"
},
{
"path": "src/containers/SettingsModal/constants.ts",
"chars": 526,
"preview": "import { map } from 'lodash';\n\nimport { providers } from '../../providers';\nimport { CacheSettings, NightMode } from './"
},
{
"path": "src/containers/SettingsModal/index.ts",
"chars": 49,
"preview": "export { SettingsModal } from './SettingsModal';\n"
},
{
"path": "src/containers/SettingsModal/tabs/Cache.tsx",
"chars": 944,
"preview": "import React from 'react';\n\nimport { Button, ButtonType } from '../../../components/Button';\nimport { toast } from '../."
},
{
"path": "src/containers/SettingsModal/tabs/NightMode.tsx",
"chars": 1676,
"preview": "import React, { useEffect, useState } from 'react';\n\nimport { RadioCard } from '../../../components/RadioCard';\nimport {"
},
{
"path": "src/containers/SettingsModal/tabs/index.ts",
"chars": 82,
"preview": "export { NightMode } from './NightMode';\nexport { CacheSettings } from './Cache';\n"
},
{
"path": "src/containers/SettingsModal/types.ts",
"chars": 107,
"preview": "export interface ISettingView {\n id: string;\n label: string;\n component: any;\n isProvider?: boolean;\n}\n"
},
{
"path": "src/containers/index.ts",
"chars": 141,
"preview": "export { RepoCard } from './RepoCard';\nexport { FilterEditModal } from './FilterEditModal';\nexport { SettingsModal } fro"
},
{
"path": "src/errors/InvalidCredentials.ts",
"chars": 186,
"preview": "import ExtendableError from 'es6-error';\n\nexport class InvalidCredentials extends ExtendableError {\n constructor(messag"
},
{
"path": "src/errors/NeedTokenError.ts",
"chars": 194,
"preview": "import ExtendableError from 'es6-error';\n\nexport class NeedTokenError extends ExtendableError {\n constructor(message = "
},
{
"path": "src/errors/RateLimitError.ts",
"chars": 312,
"preview": "import ExtendableError from 'es6-error';\n\nexport class RateLimitError extends ExtendableError {\n public remainingRateLi"
},
{
"path": "src/errors/index.ts",
"chars": 161,
"preview": "export { NeedTokenError } from './NeedTokenError';\nexport { RateLimitError } from './RateLimitError';\nexport { InvalidCr"
},
{
"path": "src/index.html",
"chars": 449,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"UTF-8\">\n <title>OctoLenses - Github Dashboard</title>\n "
},
{
"path": "src/index.scss",
"chars": 605,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@import url('https://fonts.googleapis.com/css?family=Open+Sa"
},
{
"path": "src/index.tsx",
"chars": 513,
"preview": "import 'babel-polyfill';\nimport { Provider } from 'mobx-react';\nimport React from 'react';\nimport ReactDOM from 'react-d"
},
{
"path": "src/lib/assertUnreachable.ts",
"chars": 94,
"preview": "export function assertUnreachable<T>(_: never, defaultValue: T): T {\n return defaultValue;\n}\n"
},
{
"path": "src/lib/cache.ts",
"chars": 2835,
"preview": "import { chain, startsWith } from 'lodash';\n\ninterface ICacheEntry<T> {\n expiresAt: number;\n value: T;\n}\n\nexport class"
},
{
"path": "src/lib/github/index.ts",
"chars": 49,
"preview": "export { fetchTrendingRepos } from './trending';\n"
},
{
"path": "src/lib/github/trending/index.ts",
"chars": 937,
"preview": "import hash from 'object-hash';\n\nimport { Cache } from '../../../lib/cache';\nimport { client } from '../../../providers/"
},
{
"path": "src/migrations/index.ts",
"chars": 837,
"preview": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport { IMigration } from './types';\nimport v0_to_v1 from './v0-to-v"
},
{
"path": "src/migrations/mocks/index.ts",
"chars": 477,
"preview": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport v0 from './v0';\nimport v1 from './v1';\nimport v1_withoutToken "
},
{
"path": "src/migrations/mocks/v0.ts",
"chars": 961,
"preview": "export default {\n filtersStore: {\n data: [\n {\n id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n label"
},
{
"path": "src/migrations/mocks/v1-without-token.ts",
"chars": 961,
"preview": "export default {\n filtersStore: {\n data: [\n {\n id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n label"
},
{
"path": "src/migrations/mocks/v1.ts",
"chars": 983,
"preview": "export default {\n filtersStore: {\n data: [\n {\n id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n label"
},
{
"path": "src/migrations/mocks/v2-with-negated-predicates.ts",
"chars": 707,
"preview": "export default {\n filtersStore: {\n data: [\n {\n id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n label"
},
{
"path": "src/migrations/mocks/v2.ts",
"chars": 1089,
"preview": "export default {\n filtersStore: {\n data: [\n {\n id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n label"
},
{
"path": "src/migrations/mocks/v3-with-defaulted-operators.ts",
"chars": 702,
"preview": "export default {\n filtersStore: {\n data: [\n {\n id: '5c2382a0-5216-11e9-ad7a-73a635a52ed2',\n label"
},
{
"path": "src/migrations/testing-utils.ts",
"chars": 1571,
"preview": "/* tslint:disable no-console */\n\nimport { forEach, keys, pick, reduce } from 'lodash';\n\nimport { migrator } from './inde"
},
{
"path": "src/migrations/types.ts",
"chars": 87,
"preview": "export interface IMigration {\n name: string;\n shouldRun(): boolean;\n run(): void;\n}\n"
},
{
"path": "src/migrations/utils.ts",
"chars": 677,
"preview": "export function getFromLocalStorage(key: string) {\n const data = localStorage.getItem(key);\n\n if (data) {\n return J"
},
{
"path": "src/migrations/v0-to-v1.test.ts",
"chars": 882,
"preview": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport {\n dumpLocalStorageToObject,\n hydrateLocalStorageFromObject,"
},
{
"path": "src/migrations/v0-to-v1.ts",
"chars": 641,
"preview": "/* tslint:disable no-console */\n\nimport { IMigration } from './types';\nimport { getFromLocalStorage, saveToLocalStorage "
},
{
"path": "src/migrations/v1-to-v2.test.ts",
"chars": 1941,
"preview": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport {\n dumpLocalStorageToObject,\n hydrateLocalStorageFromObject,"
},
{
"path": "src/migrations/v1-to-v2.ts",
"chars": 1661,
"preview": "/* tslint:disable no-console */\n\nimport { ProviderType } from '../providers';\nimport { Filter } from '../store/filters';"
},
{
"path": "src/migrations/v2-to-v3.test.ts",
"chars": 972,
"preview": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport {\n dumpLocalStorageToObject,\n hydrateLocalStorageFromObject,"
},
{
"path": "src/migrations/v2-to-v3.ts",
"chars": 1790,
"preview": "/* tslint:disable no-console */\n\nimport { providers, ProviderType } from '../providers';\nimport { Filter } from '../stor"
},
{
"path": "src/pages/Dashboard/Dashboard.tsx",
"chars": 7838,
"preview": "import cx from 'classnames';\nimport ExtendableError from 'es6-error';\nimport { get, size } from 'lodash';\nimport { compu"
},
{
"path": "src/pages/Dashboard/FilterLinkContainer.tsx",
"chars": 734,
"preview": "import React from 'react';\nimport { SortableContainer } from 'react-sortable-hoc';\n\nimport { FilterLink } from '../../co"
},
{
"path": "src/pages/Dashboard/index.ts",
"chars": 41,
"preview": "export { Dashboard } from './Dashboard';\n"
},
{
"path": "src/pages/Discover/Discover.scss",
"chars": 459,
"preview": ".Discover {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n\n &__Actions {\n display: flex;"
},
{
"path": "src/pages/Discover/Discover.tsx",
"chars": 1739,
"preview": "import { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose } from 'recompose';\n\nimport {"
},
{
"path": "src/pages/Discover/index.ts",
"chars": 39,
"preview": "export { Discover } from './Discover';\n"
},
{
"path": "src/pages/index.ts",
"chars": 80,
"preview": "export { Discover } from './Discover';\nexport { Dashboard } from './Dashboard';\n"
},
{
"path": "src/providers/AbstractProvider.ts",
"chars": 1860,
"preview": "import { observable } from 'mobx';\nimport { persist } from 'mobx-persist';\n\nimport { Filter } from '../store/filters';\ni"
},
{
"path": "src/providers/github/components/IssueCard/CheckStatusIndicator.tsx",
"chars": 832,
"preview": "import cx from 'classnames';\nimport React from 'react';\n\nimport { IIssue } from './IssueCard';\nimport { IssueStatus } fr"
},
{
"path": "src/providers/github/components/IssueCard/ConflictIndicator.tsx",
"chars": 332,
"preview": "import React from 'react';\n\ninterface IProps {\n conflicting: boolean;\n}\n\nexport const ConflictIndicator = ({ conflictin"
},
{
"path": "src/providers/github/components/IssueCard/ContextualDropdown.tsx",
"chars": 2508,
"preview": "import cx from 'classnames';\nimport ClipboardJS from 'clipboard';\nimport { inject, observer } from 'mobx-react';\nimport "
},
{
"path": "src/providers/github/components/IssueCard/IssueCard.tsx",
"chars": 4854,
"preview": "import cx from 'classnames';\nimport { get } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport React f"
},
{
"path": "src/providers/github/components/IssueCard/IssueStatusIndicator.tsx",
"chars": 867,
"preview": "import cx from 'classnames';\nimport React from 'react';\n\nimport { IIssue } from './IssueCard';\nimport {assertUnreachable"
},
{
"path": "src/providers/github/components/IssueCard/LabelBadge.tsx",
"chars": 534,
"preview": "import cx from 'classnames';\nimport contrast from 'contrast';\nimport React from 'react';\n\ninterface IProps {\n label: {\n"
},
{
"path": "src/providers/github/components/IssueCard/index.ts",
"chars": 41,
"preview": "export { IssueCard } from './IssueCard';\n"
},
{
"path": "src/providers/github/components/IssueCard/types.ts",
"chars": 704,
"preview": "export enum IssueStatus {\n PENDING = 'PENDING',\n SUCCESS = 'SUCCESS',\n FAILURE = 'FAILURE',\n UNKNOWN = 'UNKNOWN',\n}\n"
},
{
"path": "src/providers/github/components/ProfileCard.tsx",
"chars": 1216,
"preview": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose "
},
{
"path": "src/providers/github/components/Settings.tsx",
"chars": 2835,
"preview": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React, { useState } from 'react';\nimp"
},
{
"path": "src/providers/github/fetchers/client.ts",
"chars": 1686,
"preview": "import { get, pickBy } from 'lodash';\n\nimport {\n InvalidCredentials,\n NeedTokenError,\n RateLimitError,\n} from '../../"
},
{
"path": "src/providers/github/fetchers/graphql/query.ts",
"chars": 1627,
"preview": "const commonFields = `\n url\n title\n state\n number\n createdAt\n author {\n url\n login\n avatarUrl\n }\n label"
},
{
"path": "src/providers/github/fetchers/graphql/search.ts",
"chars": 1375,
"preview": "import { chain, omit, map } from 'lodash';\n\nimport { Cache } from '../../../../lib/cache';\nimport { Filter } from '../.."
},
{
"path": "src/providers/github/fetchers/graphql/utils.ts",
"chars": 1366,
"preview": "import { get, map } from 'lodash';\n\nimport { IssueStatus } from '../../components/IssueCard/types';\n\nexport const COMMIT"
},
{
"path": "src/providers/github/fetchers/index.ts",
"chars": 401,
"preview": "import { IGithubSettings } from '..';\nimport { Filter } from '../../../store/filters';\nimport { search as graphqlSearch "
},
{
"path": "src/providers/github/fetchers/rest/profile.ts",
"chars": 142,
"preview": "import { client } from '../client';\n\nexport const fetchProfile = async (token: string) => {\n return client({ endpoint: "
},
{
"path": "src/providers/github/fetchers/rest/search.ts",
"chars": 2068,
"preview": "import { chain, map, pick } from 'lodash';\n\nimport { Cache } from '../../../../lib/cache';\nimport { Filter } from '../.."
},
{
"path": "src/providers/github/index.tsx",
"chars": 1719,
"preview": "import { find } from 'lodash';\nimport { action, observable } from 'mobx';\nimport React from 'react';\n\nimport { Filter } "
},
{
"path": "src/providers/github/predicates/createdOrUpdatedAt.ts",
"chars": 1985,
"preview": "import moment from 'moment';\nimport { IDropdownPredicate, PredicateType } from '../../types';\n\nenum Preset {\n ONE_HOUR_"
},
{
"path": "src/providers/github/predicates/draft.ts",
"chars": 338,
"preview": "import { IDropdownPredicate, PredicateType } from '../../types';\n\nexport const draft: IDropdownPredicate = {\n name: 'dr"
},
{
"path": "src/providers/github/predicates/index.ts",
"chars": 2856,
"preview": "import { capitalize } from 'lodash';\n\nimport { Predicate, PredicateType } from '../../types';\nimport { mergeStatus } fro"
},
{
"path": "src/providers/github/predicates/mergeStatus.ts",
"chars": 365,
"preview": "import { IDropdownPredicate, PredicateType } from '../../types';\n\nexport const mergeStatus: IDropdownPredicate = {\n nam"
},
{
"path": "src/providers/github/predicates/review.ts",
"chars": 465,
"preview": "import { IDropdownPredicate, PredicateType } from '../../types';\n\nexport const review: IDropdownPredicate = {\n name: 'r"
},
{
"path": "src/providers/github/predicates/status.ts",
"chars": 340,
"preview": "import { IDropdownPredicate, PredicateType } from '../../types';\n\nexport const status: IDropdownPredicate = {\n name: 's"
},
{
"path": "src/providers/github/predicates/type.ts",
"chars": 332,
"preview": "import { IDropdownPredicate, PredicateType } from '../../types';\n\nexport const type: IDropdownPredicate = {\n name: 'typ"
},
{
"path": "src/providers/index.ts",
"chars": 280,
"preview": "import { github } from './github';\nimport { jira } from './jira';\nimport { ProviderType } from './types';\n\nexport { Abst"
},
{
"path": "src/providers/jira/components/AvailableResources.tsx",
"chars": 1392,
"preview": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose "
},
{
"path": "src/providers/jira/components/IssueCard/IssueCard.tsx",
"chars": 3399,
"preview": "import cx from 'classnames';\nimport { inject, observer } from 'mobx-react';\nimport React from 'react';\nimport { compose "
},
{
"path": "src/providers/jira/components/IssueCard/StatusBadge.tsx",
"chars": 727,
"preview": "import cx from 'classnames';\nimport React from 'react';\n\nimport { IJiraIssue } from './IssueCard';\n\ninterface IProps {\n "
},
{
"path": "src/providers/jira/components/IssueCard/index.ts",
"chars": 84,
"preview": "export { IssueCard } from './IssueCard';\nexport type { IProps } from './IssueCard';\n"
},
{
"path": "src/providers/jira/components/LoginButton.tsx",
"chars": 3885,
"preview": "import cx from 'classnames';\nimport { chain } from 'lodash';\nimport { inject, observer } from 'mobx-react';\nimport React"
},
{
"path": "src/providers/jira/components/LogoutButton.tsx",
"chars": 735,
"preview": "import { observer } from 'mobx-react';\nimport React from 'react';\n\nimport { JiraProvider } from '..';\nimport { Button, B"
},
{
"path": "src/providers/jira/components/Settings.tsx",
"chars": 624,
"preview": "import { observer } from 'mobx-react';\nimport React from 'react';\n\nimport { JiraProvider } from '..';\nimport { Available"
},
{
"path": "src/providers/jira/fetchers/index.ts",
"chars": 1007,
"preview": "import { chain, get } from 'lodash';\nimport hash from 'object-hash';\n\nimport { IJiraResource, IJiraSettings } from '..';"
},
{
"path": "src/providers/jira/fetchers/refreshToken.ts",
"chars": 453,
"preview": "interface IRefreshResult {\n access_token: string;\n expires_in: number;\n}\n\nexport const refreshToken = async (\n refres"
},
{
"path": "src/providers/jira/fetchers/resources.ts",
"chars": 337,
"preview": "export const fetchResources = async (token: string) => {\n const url = 'https://api.atlassian.com/oauth/token/accessible"
},
{
"path": "src/providers/jira/fetchers/swapToken.ts",
"chars": 512,
"preview": "export interface ISwapResult {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n}\n\nexport const sw"
},
{
"path": "src/providers/jira/index.tsx",
"chars": 3089,
"preview": "/* eslint-disable @typescript-eslint/camelcase */\n\nimport { find, get } from 'lodash';\nimport { action, computed, observ"
},
{
"path": "src/providers/jira/predicates/index.ts",
"chars": 3225,
"preview": "import { capitalize, chain } from 'lodash';\n\nimport { Predicate, PredicateType } from '../../types';\n\nenum JiraOperators"
},
{
"path": "src/providers/types.ts",
"chars": 1062,
"preview": "export enum ProviderType {\n GITHUB = 'github',\n JIRA = 'jira',\n}\n\ntype PredicateIdentifier = string;\n\n// Template pred"
},
{
"path": "src/service_worker/cache.js",
"chars": 1360,
"preview": "import 'babel-polyfill';\nimport { difference } from 'lodash';\n\nconst PRECACHE = 'precache-v1';\nconst RUNTIME = 'runtime'"
},
{
"path": "src/service_worker/index.js",
"chars": 244,
"preview": "import './cache.js';\n\nfunction openInNewTab() {\n chrome.tabs.create({\n url: chrome.runtime.getURL('index.html'),\n }"
},
{
"path": "src/setupTests.ts",
"chars": 74,
"preview": "require('jest-localstorage-mock'); // tslint:disable-line no-var-requires\n"
},
{
"path": "src/store/filters.ts",
"chars": 2270,
"preview": "import { find, findIndex } from 'lodash';\nimport { action, computed, observable } from 'mobx';\nimport { persist } from '"
},
{
"path": "src/store/index.ts",
"chars": 2106,
"preview": "import { chain } from 'lodash';\nimport { create } from 'mobx-persist';\n\nimport { Cache } from '../lib/cache';\nimport { m"
},
{
"path": "src/store/models/filter.ts",
"chars": 4090,
"preview": "import { difference, map, merge } from 'lodash';\nimport { action, computed, observable, reaction } from 'mobx';\nimport {"
},
{
"path": "src/store/navigation.ts",
"chars": 311,
"preview": "import { action, observable } from 'mobx';\nimport { persist } from 'mobx-persist';\n\nexport class NavigationStore {\n @pe"
},
{
"path": "src/store/settings.ts",
"chars": 2362,
"preview": "import { action, autorun, observable } from 'mobx';\nimport { persist } from 'mobx-persist';\n\nimport { DARK_MODE } from '"
},
{
"path": "src/store/trends.ts",
"chars": 858,
"preview": "import { action, observable } from 'mobx';\n\nimport { getDateFromValue } from '../constants/dates';\nimport { fetchTrendin"
},
{
"path": "tailwind.config.js",
"chars": 3381,
"preview": "module.exports = {\n theme: {\n fontFamily: {\n open: ['Open Sans', 'sans-serif'],\n roboto: ['Roboto', 'sans-"
},
{
"path": "tsconfig.json",
"chars": 726,
"preview": "{\n \"compilerOptions\": {\n \"outDir\": \"./dist/\",\n \"allowJs\": true,\n \"allowSyntheticDefaultImports\": true,\n \"es"
}
]
About this extraction
This page contains the full source code of the rgehan/octolenses GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 159 files (210.7 KB), approximately 62.4k tokens, and a symbol index with 263 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.